diff --git a/AUTHORS b/AUTHORS index 1e56bd3412..e1966c5af5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -232,6 +232,7 @@ answer newbie questions, and generally made Django that much better: phil@produxion.net phil.h.smith@gmail.com Gustavo Picon + pigletto Luke Plant plisk Daniel Poelzleithner @@ -262,6 +263,7 @@ answer newbie questions, and generally made Django that much better: SmileyChris smurf@smurf.noris.de sopel + Leo Soto Wiliam Alves de Souza Georgi Stanojevski Vasiliy Stavenko @@ -274,6 +276,7 @@ answer newbie questions, and generally made Django that much better: Swaroop C H Aaron Swartz Ville Säävuori + Tyler Tarabula Tyson Tate Frank Tegtmeyer thebjorn diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css index bf526bfd66..67d9662f3e 100644 --- a/django/contrib/admin/media/css/widgets.css +++ b/django/contrib/admin/media/css/widgets.css @@ -43,7 +43,7 @@ p.file-upload { line-height:20px; margin:0; padding:0; color:#666; font-size:11p /* CALENDARS & CLOCKS */ .calendarbox, .clockbox { margin:5px auto; font-size:11px; width:16em; text-align:center; background:white; position:relative; } -.clockbox { width:9em; } +.clockbox { width:auto; } .calendar { margin:0; padding: 0; } .calendar table { margin:0; padding:0; border-collapse:collapse; background:white; width:99%; } .calendar caption, .calendarbox h2 { margin: 0; font-size:11px; text-align:center; border-top:none; } diff --git a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js index b1504fc819..4682a6841a 100644 --- a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js @@ -195,6 +195,19 @@ var DateTimeShortcuts = { openCalendar: function(num) { var cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1+num) var cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName+num) + var inp = DateTimeShortcuts.calendarInputs[num]; + + // Determine if the current value in the input has a valid date. + // If so, draw the calendar with that date's year and month. + if (inp.value) { + var date_parts = inp.value.split('-'); + var year = date_parts[0]; + var month = parseFloat(date_parts[1]); + if (year.match(/\d\d\d\d/) && month >= 1 && month <= 12) { + DateTimeShortcuts.calendars[num].drawDate(month, year); + } + } + // Recalculate the clockbox position // is it left-to-right or right-to-left layout ? diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 2fb4a6f510..de0dc21c5d 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -2,7 +2,7 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.http import HttpResponseRedirect from urllib import quote -def user_passes_test(test_func, login_url=None): +def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): """ Decorator for views that checks that the user passes the given test, redirecting to the log-in page if necessary. The test should be a callable @@ -15,20 +15,25 @@ def user_passes_test(test_func, login_url=None): def _checklogin(request, *args, **kwargs): if test_func(request.user): return view_func(request, *args, **kwargs) - return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, quote(request.get_full_path()))) + return HttpResponseRedirect('%s?%s=%s' % (login_url, redirect_field_name, quote(request.get_full_path()))) _checklogin.__doc__ = view_func.__doc__ _checklogin.__dict__ = view_func.__dict__ return _checklogin return _dec -login_required = user_passes_test(lambda u: u.is_authenticated()) -login_required.__doc__ = ( +def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME): """ Decorator for views that checks that the user is logged in, redirecting to the log-in page if necessary. """ + actual_decorator = user_passes_test( + lambda u: u.is_authenticated(), + redirect_field_name=redirect_field_name ) + if function: + return actual_decorator(function) + return actual_decorator def permission_required(perm, login_url=None): """ diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index f1129379d6..d3d8b4ccb7 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -9,10 +9,10 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth import REDIRECT_FIELD_NAME from django.utils.translation import ugettext as _ -def login(request, template_name='registration/login.html'): +def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME): "Displays the login form and handles the login action." manipulator = AuthenticationForm(request) - redirect_to = request.REQUEST.get(REDIRECT_FIELD_NAME, '') + redirect_to = request.REQUEST.get(redirect_field_name, '') if request.POST: errors = manipulator.get_validation_errors(request.POST) if not errors: @@ -35,7 +35,7 @@ def login(request, template_name='registration/login.html'): return render_to_response(template_name, { 'form': oldforms.FormWrapper(manipulator, request.POST, errors), - REDIRECT_FIELD_NAME: redirect_to, + redirect_field_name: redirect_to, 'site_name': current_site.name, }, context_instance=RequestContext(request)) @@ -56,12 +56,12 @@ def logout_then_login(request, login_url=None): login_url = settings.LOGIN_URL return logout(request, login_url) -def redirect_to_login(next, login_url=None): +def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): "Redirects the user to the login page, passing the given 'next' page" if not login_url: from django.conf import settings login_url = settings.LOGIN_URL - return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, next)) + return HttpResponseRedirect('%s?%s=%s' % (login_url, redirect_field_name, next)) def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html'): diff --git a/django/contrib/localflavor/ca/__init__.py b/django/contrib/localflavor/ca/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/ca/ca_provinces.py b/django/contrib/localflavor/ca/ca_provinces.py new file mode 100644 index 0000000000..072159ad57 --- /dev/null +++ b/django/contrib/localflavor/ca/ca_provinces.py @@ -0,0 +1,57 @@ +""" +An alphabetical list of provinces and territories for use as `choices` +in a formfield., and a mapping of province misspellings/abbreviations to +normalized abbreviations + +Source: http://www.canada.gc.ca/othergov/prov_e.html + +This exists in this standalone file so that it's only imported into memory +when explicitly needed. +""" + +PROVINCE_CHOICES = ( + ('AB', 'Alberta'), + ('BC', 'British Columbia'), + ('MB', 'Manitoba'), + ('NB', 'New Brunswick'), + ('NF', 'Newfoundland and Labrador'), + ('NT', 'Northwest Territories'), + ('NS', 'Nova Scotia'), + ('NU', 'Nunavut'), + ('ON', 'Ontario'), + ('PE', 'Prince Edward Island'), + ('QC', 'Quebec'), + ('SK', 'Saskatchewan'), + ('YK', 'Yukon') +) + +PROVINCES_NORMALIZED = { + 'ab': 'AB', + 'alberta': 'AB', + 'bc': 'BC', + 'b.c.': 'BC', + 'british columbia': 'BC', + 'mb': 'MB', + 'manitoba': 'MB', + 'nf': 'NF', + 'newfoundland': 'NF', + 'newfoundland and labrador': 'NF', + 'nt': 'NT', + 'northwest territories': 'NT', + 'ns': 'NS', + 'nova scotia': 'NS', + 'nu': 'NU', + 'nunavut': 'NU', + 'on': 'ON', + 'ontario': 'ON', + 'pe': 'PE', + 'pei': 'PE', + 'p.e.i.': 'PE', + 'prince edward island': 'PE', + 'qc': 'QC', + 'quebec': 'QC', + 'sk': 'SK', + 'saskatchewan': 'SK', + 'yk': 'YK', + 'yukon': 'YK', +} \ No newline at end of file diff --git a/django/contrib/localflavor/ca/forms.py b/django/contrib/localflavor/ca/forms.py new file mode 100644 index 0000000000..98f65a5c6c --- /dev/null +++ b/django/contrib/localflavor/ca/forms.py @@ -0,0 +1,112 @@ +""" +Canada-specific Form helpers +""" + +from django.newforms import ValidationError +from django.newforms.fields import Field, RegexField, Select, EMPTY_VALUES +from django.newforms.util import smart_unicode +from django.utils.translation import gettext, ugettext +import re + +phone_digits_re = re.compile(r'^(?:1-?)?(\d{3})[-\.]?(\d{3})[-\.]?(\d{4})$') +sin_re = re.compile(r"^(\d{3})-(\d{3})-(\d{3})$") + +class CAPostalCodeField(RegexField): + """Canadian postal code field.""" + def __init__(self, *args, **kwargs): + super(CAPostalCodeField, self).__init__(r'^[ABCEGHJKLMNPRSTVXYZ]\d[A-Z] \d[A-Z]\d$', + max_length=None, min_length=None, + error_message=gettext(u'Enter a postal code in the format XXX XXX.'), + *args, **kwargs) + +class CAPhoneNumberField(Field): + """Canadian phone number field.""" + def clean(self, value): + """Validate a phone number. + """ + super(CAPhoneNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + value = re.sub('(\(|\)|\s+)', '', smart_unicode(value)) + m = phone_digits_re.search(value) + if m: + return u'%s-%s-%s' % (m.group(1), m.group(2), m.group(3)) + raise ValidationError(u'Phone numbers must be in XXX-XXX-XXXX format.') + +class CAProvinceField(Field): + """ + A form field that validates its input is a Canadian province name or abbreviation. + It normalizes the input to the standard two-leter postal service + abbreviation for the given province. + """ + def clean(self, value): + from ca_provinces import PROVINCES_NORMALIZED + super(CAProvinceField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + try: + value = value.strip().lower() + except AttributeError: + pass + else: + try: + return PROVINCES_NORMALIZED[value.strip().lower()].decode('ascii') + except KeyError: + pass + raise ValidationError(u'Enter a Canadian province or territory.') + +class CAProvinceSelect(Select): + """ + A Select widget that uses a list of Canadian provinces and + territories as its choices. + """ + def __init__(self, attrs=None): + from ca_provinces import PROVINCE_CHOICES # relative import + super(CAProvinceSelect, self).__init__(attrs, choices=PROVINCE_CHOICES) + +class CASocialInsuranceNumberField(Field): + """ + A Canadian Social Insurance Number (SIN). + + Checks the following rules to determine whether the number is valid: + + * Conforms to the XXX-XXX-XXXX format. + * Passes the check digit process "Luhn Algorithm" + See: http://en.wikipedia.org/wiki/Social_Insurance_Number + """ + def clean(self, value): + super(CASocialInsuranceNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + msg = ugettext('Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.') + match = re.match(sin_re, value) + if not match: + raise ValidationError(msg) + + number = u'%s-%s-%s' % (match.group(1), match.group(2), match.group(3)) + check_number = u'%s%s%s' % (match.group(1), match.group(2), match.group(3)) + if not self.luhn_checksum_is_valid(check_number): + raise ValidationError(msg) + return number + + def luhn_checksum_is_valid(self, number): + """ + Checks to make sure that the SIN passes a luhn mod-10 checksum + See: http://en.wikipedia.org/wiki/Luhn_algorithm + """ + + sum = 0 + num_digits = len(number) + oddeven = num_digits & 1 + + for count in range(0, num_digits): + digit = int(number[count]) + + if not (( count & 1 ) ^ oddeven ): + digit = digit * 2 + if digit > 9: + digit = digit - 9 + + sum = sum + digit + + return ( (sum % 10) == 0 ) \ No newline at end of file diff --git a/django/contrib/localflavor/generic/__init__.py b/django/contrib/localflavor/generic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/localflavor/generic/forms.py b/django/contrib/localflavor/generic/forms.py new file mode 100644 index 0000000000..a6d813723c --- /dev/null +++ b/django/contrib/localflavor/generic/forms.py @@ -0,0 +1,38 @@ +from django import newforms as forms + +DEFAULT_DATE_INPUT_FORMATS = ( + '%Y-%m-%d', '%d/%m/%Y', '%d/%m/%y', # '2006-10-25', '25/10/2006', '25/10/06' + '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' + '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' + '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' + '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' +) + +DEFAULT_DATETIME_INPUT_FORMATS = ( + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%Y-%m-%d', # '2006-10-25' + '%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59' + '%d/%m/%Y %H:%M', # '25/10/2006 14:30' + '%d/%m/%Y', # '25/10/2006' + '%d/%m/%y %H:%M:%S', # '25/10/06 14:30:59' + '%d/%m/%y %H:%M', # '25/10/06 14:30' + '%d/%m/%y', # '25/10/06' +) + +class DateField(forms.DateField): + """ + A date input field which uses non-US date input formats by default. + """ + def __init__(self, input_formats=None, *args, **kwargs): + input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS + super(DateField, self).__init__(input_formats=input_formats, *args, **kwargs) + +class DateTimeField(forms.DateTimeField): + """ + A date and time input field which uses non-US date and time input formats + by default. + """ + def __init__(self, input_formats=None, *args, **kwargs): + input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS + super(DateTimeField, self).__init__(input_formats=input_formats, *args, **kwargs) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 768fc14b00..21737f682f 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -142,7 +142,7 @@ def fix_location_header(request, response): Code constructing response objects is free to insert relative paths and this function converts them to absolute paths. """ - if 'Location' in response.headers and http.get_host(request): + if 'location' in response.headers and http.get_host(request): response['Location'] = request.build_absolute_uri(response['Location']) return response diff --git a/django/core/management/commands/adminindex.py b/django/core/management/commands/adminindex.py index e3dd493fd3..4f389136ca 100644 --- a/django/core/management/commands/adminindex.py +++ b/django/core/management/commands/adminindex.py @@ -1,4 +1,5 @@ from django.core.management.base import AppCommand +from django.utils.encoding import force_unicode from django.utils.text import capfirst MODULE_TEMPLATE = ''' {%% if perms.%(app)s.%(addperm)s or perms.%(app)s.%(changeperm)s %%} @@ -24,7 +25,7 @@ class Command(AppCommand): output.append(MODULE_TEMPLATE % { 'app': app_label, 'mod': model._meta.module_name, - 'name': capfirst(model._meta.verbose_name_plural), + 'name': force_unicode(capfirst(model._meta.verbose_name_plural)), 'addperm': model._meta.get_add_permission(), 'changeperm': model._meta.get_change_permission(), }) diff --git a/django/core/management/commands/testserver.py b/django/core/management/commands/testserver.py index 50a10a12bc..9b169d3d9b 100644 --- a/django/core/management/commands/testserver.py +++ b/django/core/management/commands/testserver.py @@ -7,6 +7,9 @@ class Command(BaseCommand): make_option('--verbosity', action='store', dest='verbosity', default='1', type='choice', choices=['0', '1', '2'], help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'), + make_option('--addrport', action='store', dest='addrport', + type='string', default='', + help='port number or ipaddr:port to run the server on'), ) help = 'Runs a development server with data from the given fixture(s).' args = '[fixture ...]' @@ -19,6 +22,7 @@ class Command(BaseCommand): from django.test.utils import create_test_db verbosity = int(options.get('verbosity', 1)) + addrport = options.get('addrport') # Create a test database. db_name = create_test_db(verbosity=verbosity) @@ -30,4 +34,4 @@ class Command(BaseCommand): # a strange error -- it causes this handle() method to be called # multiple times. shutdown_message = '\nServer stopped.\nNote that the test database, %r, has not been deleted. You can explore it on your own.' % db_name - call_command('runserver', shutdown_message=shutdown_message, use_reloader=False) + call_command('runserver', addrport=addrport, shutdown_message=shutdown_message, use_reloader=False) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index aa4ef47c2f..6528a34f29 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -1,5 +1,6 @@ import sys from django.core.management.color import color_style +from django.utils.itercompat import is_iterable class ModelErrorCollection: def __init__(self, outfile=sys.stdout): @@ -51,7 +52,8 @@ def get_validation_errors(outfile, app=None): if f.prepopulate_from is not None and type(f.prepopulate_from) not in (list, tuple): e.add(opts, '"%s": prepopulate_from should be a list or tuple.' % f.name) if f.choices: - if not hasattr(f.choices, '__iter__'): + if isinstance(f.choices, basestring) or \ + not is_iterable(f.choices): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) else: for c in f.choices: diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 76e5743539..dbb1d032eb 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -438,21 +438,6 @@ class FormatStylePlaceholderCursor(Database.Cursor): """ charset = 'utf-8' - def _rewrite_args(self, query, params=None): - if params is None: - params = [] - else: - params = self._format_params(params) - args = [(':arg%d' % i) for i in range(len(params))] - query = smart_str(query, self.charset) % tuple(args) - # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it - # it does want a trailing ';' but not a trailing '/'. However, these - # characters must be included in the original query in case the query - # is being passed to SQL*Plus. - if query.endswith(';') or query.endswith('/'): - query = query[:-1] - return query, params - def _format_params(self, params): if isinstance(params, dict): result = {} @@ -464,12 +449,35 @@ class FormatStylePlaceholderCursor(Database.Cursor): return tuple([smart_str(p, self.charset, True) for p in params]) def execute(self, query, params=None): - query, params = self._rewrite_args(query, params) + if params is None: + params = [] + else: + params = self._format_params(params) + args = [(':arg%d' % i) for i in range(len(params))] + # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it + # it does want a trailing ';' but not a trailing '/'. However, these + # characters must be included in the original query in case the query + # is being passed to SQL*Plus. + if query.endswith(';') or query.endswith('/'): + query = query[:-1] + query = smart_str(query, self.charset) % tuple(args) return Database.Cursor.execute(self, query, params) def executemany(self, query, params=None): - query, params = self._rewrite_args(query, params) - return Database.Cursor.executemany(self, query, params) + try: + args = [(':arg%d' % i) for i in range(len(params[0]))] + except (IndexError, TypeError): + # No params given, nothing to do + return None + # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it + # it does want a trailing ';' but not a trailing '/'. However, these + # characters must be included in the original query in case the query + # is being passed to SQL*Plus. + if query.endswith(';') or query.endswith('/'): + query = query[:-1] + query = smart_str(query, self.charset) % tuple(args) + new_param_list = [self._format_params(i) for i in params] + return Database.Cursor.executemany(self, query, new_param_list) def fetchone(self): return to_unicode(Database.Cursor.fetchone(self)) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index a482a240cf..b4b445cd16 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -133,8 +133,12 @@ class SQLiteCursorWrapper(Database.Cursor): return Database.Cursor.execute(self, query, params) def executemany(self, query, param_list): - query = self.convert_query(query, len(param_list[0])) - return Database.Cursor.executemany(self, query, param_list) + try: + query = self.convert_query(query, len(param_list[0])) + return Database.Cursor.executemany(self, query, param_list) + except (IndexError,TypeError): + # No parameter list provided + return None def convert_query(self, query, num_params): return query % tuple("?" * num_params) diff --git a/django/db/models/base.py b/django/db/models/base.py index beb413fc4c..b728150a40 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -12,7 +12,7 @@ from django.db.models.loading import register_models, get_model from django.dispatch import dispatcher from django.utils.datastructures import SortedDict from django.utils.functional import curry -from django.utils.encoding import smart_str, force_unicode +from django.utils.encoding import smart_str, force_unicode, smart_unicode from django.conf import settings from itertools import izip import types @@ -213,7 +213,7 @@ class Model(object): pk_val = self._get_pk_val() # Note: the comparison with '' is required for compatibility with # oldforms-style model creation. - pk_set = pk_val is not None and pk_val != u'' + pk_set = pk_val is not None and smart_unicode(pk_val) != u'' record_exists = True if pk_set: # Determine whether a record with the primary key already exists. diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 7e6a7cd258..8fa091dd76 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -693,7 +693,8 @@ class DecimalField(Field): class EmailField(CharField): def __init__(self, *args, **kwargs): - kwargs['max_length'] = 75 + if 'max_length' not in kwargs: + kwargs['max_length'] = 75 CharField.__init__(self, *args, **kwargs) def get_internal_type(self): diff --git a/django/db/models/options.py b/django/db/models/options.py index 502cbc4a65..788d1c80de 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -52,9 +52,19 @@ class Options(object): del meta_attrs['__doc__'] for attr_name in DEFAULT_NAMES: setattr(self, attr_name, meta_attrs.pop(attr_name, getattr(self, attr_name))) + + # unique_together can be either a tuple of tuples, or a single + # tuple of two strings. Normalize it to a tuple of tuples, so that + # calling code can uniformly expect that. + ut = meta_attrs.pop('unique_together', getattr(self, 'unique_together')) + if ut and not isinstance(ut[0], (tuple, list)): + ut = (ut,) + setattr(self, 'unique_together', ut) + # verbose_name_plural is a special case because it uses a 's' # by default. setattr(self, 'verbose_name_plural', meta_attrs.pop('verbose_name_plural', string_concat(self.verbose_name, 's'))) + # Any leftover attributes must be invalid. if meta_attrs != {}: raise TypeError, "'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()) diff --git a/django/http/__init__.py b/django/http/__init__.py index 2b68a6243a..1bb1621d77 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -246,7 +246,7 @@ class HttpResponse(object): else: self._container = [content] self._is_string = True - self.headers = {'Content-Type': content_type} + self.headers = {'content-type': content_type} self.cookies = SimpleCookie() if status: self.status_code = status @@ -258,24 +258,20 @@ class HttpResponse(object): + '\n\n' + self.content def __setitem__(self, header, value): - self.headers[header] = value + self.headers[header.lower()] = value def __delitem__(self, header): try: - del self.headers[header] + del self.headers[header.lower()] except KeyError: pass def __getitem__(self, header): - return self.headers[header] + return self.headers[header.lower()] def has_header(self, header): "Case-insensitive check for a header" - header = header.lower() - for key in self.headers.keys(): - if key.lower() == header: - return True - return False + return self.headers.has_key(header.lower()) def set_cookie(self, key, value='', max_age=None, expires=None, path='/', domain=None, secure=None): self.cookies[key] = value @@ -304,7 +300,7 @@ class HttpResponse(object): content = property(_get_content, _set_content) def __iter__(self): - self._iterator = self._container.__iter__() + self._iterator = iter(self._container) return self def next(self): diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py index dfb0c28abd..56a2b2634a 100644 --- a/django/shortcuts/__init__.py +++ b/django/shortcuts/__init__.py @@ -14,7 +14,8 @@ def render_to_response(*args, **kwargs): Returns a HttpResponse whose content is filled with the result of calling django.template.loader.render_to_string() with the passed arguments. """ - return HttpResponse(loader.render_to_string(*args, **kwargs)) + httpresponse_kwargs = {'mimetype': kwargs.pop('mimetype')} + return HttpResponse(loader.render_to_string(*args, **kwargs), **httpresponse_kwargs) load_and_render = render_to_response # For backwards compatibility. def _get_queryset(klass): diff --git a/django/template/__init__.py b/django/template/__init__.py index 6880fd5997..de8591ac5c 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -58,6 +58,7 @@ import re from inspect import getargspec from django.conf import settings from django.template.context import Context, RequestContext, ContextPopException +from django.utils.itercompat import is_iterable from django.utils.functional import curry, Promise from django.utils.text import smart_split from django.utils.encoding import smart_unicode, force_unicode @@ -900,7 +901,7 @@ class Library(object): if not getattr(self, 'nodelist', False): from django.template.loader import get_template, select_template - if hasattr(file_name, '__iter__'): + if not isinstance(file_name, basestring) and is_iterable(file_name): t = select_template(file_name) else: t = get_template(file_name) diff --git a/django/test/client.py b/django/test/client.py index c3e221554f..faacc5bf9e 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -16,6 +16,7 @@ from django.test import signals from django.utils.functional import curry from django.utils.encoding import smart_str from django.utils.http import urlencode +from django.utils.itercompat import is_iterable BOUNDARY = 'BoUnDaRyStRiNg' MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY @@ -74,21 +75,22 @@ def encode_multipart(boundary, data): '', value.read() ]) - elif hasattr(value, '__iter__'): - for item in value: + else: + if not isinstance(value, basestring) and is_iterable(value): + for item in value: + lines.extend([ + '--' + boundary, + 'Content-Disposition: form-data; name="%s"' % to_str(key), + '', + to_str(item) + ]) + else: lines.extend([ '--' + boundary, 'Content-Disposition: form-data; name="%s"' % to_str(key), '', - to_str(item) + to_str(value) ]) - else: - lines.extend([ - '--' + boundary, - 'Content-Disposition: form-data; name="%s"' % to_str(key), - '', - to_str(value) - ]) lines.extend([ '--' + boundary + '--', diff --git a/django/utils/itercompat.py b/django/utils/itercompat.py index 0de1b6cbe2..3742d6c5d8 100644 --- a/django/utils/itercompat.py +++ b/django/utils/itercompat.py @@ -57,3 +57,13 @@ else: tee = compat_tee if hasattr(itertools, 'groupby'): groupby = itertools.groupby + +def is_iterable(x): + "A implementation independent way of checking for iterables" + try: + iter(x) + except TypeError: + return False + else: + return True + diff --git a/docs/authentication.txt b/docs/authentication.txt index 131a8930b5..820aff2712 100644 --- a/docs/authentication.txt +++ b/docs/authentication.txt @@ -402,11 +402,29 @@ introduced in Python 2.4:: def my_view(request): # ... +In the Django development version, ``login_required`` also takes an optional +``redirect_field_name`` parameter. Example:: + + from django.contrib.auth.decorators import login_required + + def my_view(request): + # ... + my_view = login_required(redirect_field_name='redirect_to')(my_view) + +Again, an equivalent example of the more compact decorator syntax introduced in Python 2.4:: + + from django.contrib.auth.decorators import login_required + + @login_required(redirect_field_name='redirect_to') + def my_view(request): + # ... + ``login_required`` does the following: * If the user isn't logged in, redirect to ``settings.LOGIN_URL`` (``/accounts/login/`` by default), passing the current absolute URL - in the query string as ``next``. For example: + in the query string as ``next`` or the value of ``redirect_field_name``. + For example: ``/accounts/login/?next=/polls/3/``. * If the user is logged in, execute the view normally. The view code is free to assume the user is logged in. diff --git a/docs/databrowse.txt b/docs/databrowse.txt index 81e9e8e83b..72e1c71720 100644 --- a/docs/databrowse.txt +++ b/docs/databrowse.txt @@ -58,4 +58,29 @@ How to use Databrowse 4. Run the Django server and visit ``/databrowse/`` in your browser. +Requiring user login +==================== + +You can restrict access to logged-in users with only a few extra lines of +code. Simply add the following import to your URLconf:: + + from django.contrib.auth.decorators import login_required + +Then modify the URLconf so that the ``databrowse.site.root`` view is decorated +with ``login_required``:: + + (r'^databrowse/(.*)', login_required(databrowse.site.root)), + +If you haven't already added support for user logins to your URLconf, as +described in the `user authentication docs`_, then you will need to do so +now with the following mapping:: + + (r'^accounts/login/$', 'django.contrib.auth.views.login'), + +The final step is to create the login form required by +``django.contrib.auth.views.login``. The `user authentication docs`_ +provide full details and a sample template that can be used for this +purpose. + .. _template loader docs: ../templates_python/#loader-types +.. _user authentication docs: ../authentication/ diff --git a/docs/django-admin.txt b/docs/django-admin.txt index cdd31a2c48..68fcad24fe 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -627,14 +627,34 @@ This is useful in a number of ways: in any way, knowing that whatever data changes you're making are only being made to a test database. -Note that this server can only run on the default port on localhost; it does -not yet accept a ``host`` or ``port`` parameter. - -Also note that it does *not* automatically detect changes to your Python source -code (as ``runserver`` does). It does, however, detect changes to templates. +Note that this server does *not* automatically detect changes to your Python +source code (as ``runserver`` does). It does, however, detect changes to +templates. .. _unit tests: ../testing/ +--addrport [port number or ipaddr:port] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use ``--addrport`` to specify a different port, or IP address and port, from +the default of 127.0.0.1:8000. This value follows exactly the same format and +serves exactly the same function as the argument to the ``runserver`` subcommand. + +Examples: + +To run the test server on port 7000 with ``fixture1`` and ``fixture2``:: + + django-admin.py testserver --addrport 7000 fixture1 fixture2 + django-admin.py testserver fixture1 fixture2 --addrport 8080 + +(The above statements are equivalent. We include both of them to demonstrate +that it doesn't matter whether the options come before or after the +``testserver`` command.) + +To run on 1.2.3.4:7000 with a `test` fixture:: + + django-admin.py testserver --addrport 1.2.3.4:7000 test + --verbosity ~~~~~~~~~~~ diff --git a/docs/model-api.txt b/docs/model-api.txt index 27207aab6f..a8af05f676 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -221,8 +221,10 @@ The admin represents this as an ```` (a single-line input). ~~~~~~~~~~~~~~ A ``CharField`` that checks that the value is a valid e-mail address. -This doesn't accept ``max_length``; its ``max_length`` is automatically set to -75. + +In Django 0.96, this doesn't accept ``max_length``; its ``max_length`` is +automatically set to 75. In the Django development version, ``max_length`` is +set to 75 by default, but you can specify it to override default behavior. ``FileField`` ~~~~~~~~~~~~~ @@ -1224,6 +1226,13 @@ together. It's used in the Django admin and is enforced at the database level (i.e., the appropriate ``UNIQUE`` statements are included in the ``CREATE TABLE`` statement). +**New in Django development version** + +For convenience, unique_together can be a single list when dealing +with a single set of fields:: + + unique_together = ("driver", "restaurant") + ``verbose_name`` ---------------- diff --git a/docs/shortcuts.txt b/docs/shortcuts.txt new file mode 100644 index 0000000000..2c0dbb5663 --- /dev/null +++ b/docs/shortcuts.txt @@ -0,0 +1,41 @@ +========================= +Django shortcut functions +========================= + +The package ``django.shortcuts`` collects helper functions and classes that +"span" multiple levels of MVC. In other words, these functions/classes +introduce controlled coupling for convenience's sake. + +``render_to_response`` +====================== + +``django.shortcuts.render_to_response`` renders a given template with a given +context dictionary and returns an ``HttpResponse`` object with that rendered +text. + +Example:: + + from django.shortcuts import render_to_response + r = render_to_response('myapp/template.html', {'foo': 'bar'}) + +This example is equivalent to:: + + from django.http import HttpResponse + from django.template import Context, loader + t = loader.get_template('myapp/template.html') + c = Context({'foo': 'bar'}) + r = HttpResponse(t.render(c)) + +``get_object_or_404`` +===================== + +``django.shortcuts.get_object_or_404`` calls ``get()`` on a given model +manager, but it raises ``django.http.Http404`` instead of the model's +``DoesNotExist`` exception. + +``get_list_or_404`` +=================== + +``django.shortcuts.get_list_or_404`` returns the result of ``filter()`` on a +given model manager, raising ``django.http.Http404`` if the resulting list is +empty. diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py index 53bbadbfd4..375859c897 100644 --- a/tests/modeltests/custom_pk/models.py +++ b/tests/modeltests/custom_pk/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ 14. Using a custom primary key @@ -92,4 +93,8 @@ DoesNotExist: Employee matching query does not exist. >>> Business.objects.filter(employees__first_name__startswith='Fran') [] +# Primary key may be unicode string +>>> emp = Employee(employee_code='jaźń') +>>> emp.save() + """} diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 32647552eb..2df5d3cf77 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -250,6 +250,22 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_login_and_custom_redirect(self): + "Request a page that is protected with @login_required(redirect_field_name='redirect_to')" + + # Get the page without logging in. Should result in 302. + response = self.client.get('/test_client/login_protected_view_custom_redirect/') + self.assertRedirects(response, 'http://testserver/accounts/login/?redirect_to=/test_client/login_protected_view_custom_redirect/') + + # Log in + login = self.client.login(username='testclient', password='password') + self.failUnless(login, 'Could not log in') + + # Request a page that requires a login + response = self.client.get('/test_client/login_protected_view_custom_redirect/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + def test_view_with_bad_login(self): "Request a page that is protected with @login, but use bad credentials" diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py index 538c0e4b43..3779a0ecd1 100644 --- a/tests/modeltests/test_client/urls.py +++ b/tests/modeltests/test_client/urls.py @@ -13,6 +13,7 @@ urlpatterns = patterns('', (r'^form_view/$', views.form_view), (r'^form_view_with_template/$', views.form_view_with_template), (r'^login_protected_view/$', views.login_protected_view), + (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), (r'^session_view/$', views.session_view), (r'^broken_view/$', views.broken_view), (r'^mail_sending_view/$', views.mail_sending_view), diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py index e2a9081fb2..c406e17d30 100644 --- a/tests/modeltests/test_client/views.py +++ b/tests/modeltests/test_client/views.py @@ -122,6 +122,14 @@ def login_protected_view(request): return HttpResponse(t.render(c)) login_protected_view = login_required(login_protected_view) +def login_protected_view_changed_redirect(request): + "A simple view that is login protected with a custom redirect field set" + t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') + c = Context({'user': request.user}) + + return HttpResponse(t.render(c)) +login_protected_view_changed_redirect = login_required(redirect_field_name="redirect_to")(login_protected_view_changed_redirect) + def session_view(request): "A view that modifies the session" request.session['tobacconist'] = 'hovercraft' diff --git a/tests/regressiontests/backends/__init__.py b/tests/regressiontests/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py new file mode 100644 index 0000000000..50a628a22e --- /dev/null +++ b/tests/regressiontests/backends/models.py @@ -0,0 +1,29 @@ +from django.db import models + +class Square(models.Model): + root = models.IntegerField() + square = models.PositiveIntegerField() + + def __unicode__(self): + return "%s ** 2 == %s" % (self.root, self.square) + +__test__ = {'API_TESTS': """ + +#4896: Test cursor.executemany +>>> from django.db import connection +>>> cursor = connection.cursor() +>>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', +... [(i, i**2) for i in range(-5, 6)]) and None or None +>>> Square.objects.order_by('root') +[, , , , , , , , , , ] + +#4765: executemany with params=None or params=[] does nothing +>>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', None) and None or None +>>> Square.objects.count() +11 + +>>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', []) and None or None +>>> Square.objects.count() +11 + +"""} diff --git a/tests/regressiontests/forms/localflavor.py b/tests/regressiontests/forms/localflavor.py index 1d46adfc0b..2fe15847c7 100644 --- a/tests/regressiontests/forms/localflavor.py +++ b/tests/regressiontests/forms/localflavor.py @@ -1818,4 +1818,380 @@ u'' u'' >>> f.clean(u'') u'' + +# CAPostalCodeField ############################################################## + +CAPostalCodeField validates that the data is a six-character Canadian postal code. +>>> from django.contrib.localflavor.ca.forms import CAPostalCodeField +>>> f = CAPostalCodeField() +>>> f.clean('T2S 2H7') +u'T2S 2H7' +>>> f.clean('T2S 2H') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('2T6 H8I') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('T2S2H') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean(90210) +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f = CAPostalCodeField(required=False) +>>> f.clean('T2S 2H7') +u'T2S 2H7' +>>> f.clean('T2S2H7') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('T2S 2H') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('2T6 H8I') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean('T2S2H') +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean(90210) +Traceback (most recent call last): +... +ValidationError: [u'Enter a postal code in the format XXX XXX.'] +>>> f.clean(None) +u'' +>>> f.clean('') +u'' + +# CAPhoneNumberField ########################################################## + +CAPhoneNumberField validates that the data is a valid Canadian phone number, +including the area code. It's normalized to XXX-XXX-XXXX format. +Note: This test is exactly the same as the USPhoneNumberField except using a real +Candian area code + +>>> from django.contrib.localflavor.ca.forms import CAPhoneNumberField +>>> f = CAPhoneNumberField() +>>> f.clean('403-555-1212') +u'403-555-1212' +>>> f.clean('4035551212') +u'403-555-1212' +>>> f.clean('403 555-1212') +u'403-555-1212' +>>> f.clean('(403) 555-1212') +u'403-555-1212' +>>> f.clean('403 555 1212') +u'403-555-1212' +>>> f.clean('403.555.1212') +u'403-555-1212' +>>> f.clean('403.555-1212') +u'403-555-1212' +>>> f.clean(' (403) 555.1212 ') +u'403-555-1212' +>>> f.clean('555-1212') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.'] +>>> f.clean('403-55-1212') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = CAPhoneNumberField(required=False) +>>> f.clean('403-555-1212') +u'403-555-1212' +>>> f.clean('4035551212') +u'403-555-1212' +>>> f.clean('403 555-1212') +u'403-555-1212' +>>> f.clean('(403) 555-1212') +u'403-555-1212' +>>> f.clean('403 555 1212') +u'403-555-1212' +>>> f.clean('403.555.1212') +u'403-555-1212' +>>> f.clean('403.555-1212') +u'403-555-1212' +>>> f.clean(' (403) 555.1212 ') +u'403-555-1212' +>>> f.clean('555-1212') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.'] +>>> f.clean('403-55-1212') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in XXX-XXX-XXXX format.'] +>>> f.clean(None) +u'' +>>> f.clean('') +u'' + +# CAProvinceField ################################################################ + +CAProvinceField validates that the data is either an abbreviation or name of a +Canadian province. +>>> from django.contrib.localflavor.ca.forms import CAProvinceField +>>> f = CAProvinceField() +>>> f.clean('ab') +u'AB' +>>> f.clean('BC') +u'BC' +>>> f.clean('nova scotia') +u'NS' +>>> f.clean(' manitoba ') +u'MB' +>>> f.clean('T2S 2H7') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Canadian province or territory.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = CAProvinceField(required=False) +>>> f.clean('ab') +u'AB' +>>> f.clean('BC') +u'BC' +>>> f.clean('nova scotia') +u'NS' +>>> f.clean(' manitoba ') +u'MB' +>>> f.clean('T2S 2H7') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Canadian province or territory.'] +>>> f.clean(None) +u'' +>>> f.clean('') +u'' + +# CAProvinceSelect ############################################################### + +CAProvinceSelect is a Select widget that uses a list of Canadian provinces/territories +as its choices. +>>> from django.contrib.localflavor.ca.forms import CAProvinceSelect +>>> w = CAProvinceSelect() +>>> print w.render('province', 'AB') + + +# CASocialInsuranceNumberField ################################################# +>>> from django.contrib.localflavor.ca.forms import CASocialInsuranceNumberField +>>> f = CASocialInsuranceNumberField() +>>> f.clean('046-454-286') +u'046-454-286' +>>> f.clean('046-454-287') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'] +>>> f.clean('046 454 286') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'] +>>> f.clean('046-44-286') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'] + +## Generic DateField ########################################################## + +>>> from django.contrib.localflavor.generic.forms import * + +A DateField that uses generic dd/mm/yy dates instead of mm/dd/yy where +appropriate. + +>>> import datetime +>>> f = DateField() +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) +datetime.date(2006, 10, 25) +>>> f.clean('2006-10-25') +datetime.date(2006, 10, 25) +>>> f.clean('25/10/2006') +datetime.date(2006, 10, 25) +>>> f.clean('25/10/06') +datetime.date(2006, 10, 25) +>>> f.clean('Oct 25 2006') +datetime.date(2006, 10, 25) +>>> f.clean('October 25 2006') +datetime.date(2006, 10, 25) +>>> f.clean('October 25, 2006') +datetime.date(2006, 10, 25) +>>> f.clean('25 October 2006') +datetime.date(2006, 10, 25) +>>> f.clean('25 October, 2006') +datetime.date(2006, 10, 25) +>>> f.clean('2006-4-31') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('200a-10-25') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('10/25/06') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +>>> f = DateField(required=False) +>>> f.clean(None) +>>> repr(f.clean(None)) +'None' +>>> f.clean('') +>>> repr(f.clean('')) +'None' + +DateField accepts an optional input_formats parameter: +>>> f = DateField(input_formats=['%Y %m %d']) +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.date(2006, 10, 25) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.date(2006, 10, 25) +>>> f.clean('2006 10 25') +datetime.date(2006, 10, 25) + +The input_formats parameter overrides all default input formats, +so the default formats won't work unless you specify them: +>>> f.clean('2006-10-25') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('25/10/2006') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] +>>> f.clean('25/10/06') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date.'] + +## Generic DateTimeField ###################################################### + +A DateField that uses generic dd/mm/yy dates instead of mm/dd/yy where +appropriate. + +>>> import datetime +>>> f = DateTimeField() +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) +datetime.datetime(2006, 10, 25, 14, 30, 59) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) +datetime.datetime(2006, 10, 25, 14, 30, 59, 200) +>>> f.clean('2006-10-25 14:30:45') +datetime.datetime(2006, 10, 25, 14, 30, 45) +>>> f.clean('2006-10-25 14:30:00') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('2006-10-25 14:30') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('2006-10-25') +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean('25/10/2006 14:30:45') +datetime.datetime(2006, 10, 25, 14, 30, 45) +>>> f.clean('25/10/2006 14:30:00') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('25/10/2006 14:30') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('25/10/2006') +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean('25/10/06 14:30:45') +datetime.datetime(2006, 10, 25, 14, 30, 45) +>>> f.clean('25/10/06 14:30:00') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('25/10/06 14:30') +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean('25/10/06') +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean('hello') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date/time.'] +>>> f.clean('2006-10-25 4:30 p.m.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date/time.'] + +DateField accepts an optional input_formats parameter: +>>> f = DateTimeField(input_formats=['%Y %m %d %I:%M %p']) +>>> f.clean(datetime.date(2006, 10, 25)) +datetime.datetime(2006, 10, 25, 0, 0) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30)) +datetime.datetime(2006, 10, 25, 14, 30) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59)) +datetime.datetime(2006, 10, 25, 14, 30, 59) +>>> f.clean(datetime.datetime(2006, 10, 25, 14, 30, 59, 200)) +datetime.datetime(2006, 10, 25, 14, 30, 59, 200) +>>> f.clean('2006 10 25 2:30 PM') +datetime.datetime(2006, 10, 25, 14, 30) + +The input_formats parameter overrides all default input formats, +so the default formats won't work unless you specify them: +>>> f.clean('2006-10-25 14:30:45') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid date/time.'] + +>>> f = DateTimeField(required=False) +>>> f.clean(None) +>>> repr(f.clean(None)) +'None' +>>> f.clean('') +>>> repr(f.clean('')) +'None' + """