diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index 80990faa24..3d359ecf8f 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -25,7 +25,7 @@

{% endif %} -

{% blocktrans with original.username|escape as username %}Enter a new username and password for the user {{ username }}.{% endblocktrans %}

+

{% blocktrans with original.username|escape as username %}Enter a new password for the user {{ username }}.{% endblocktrans %}

diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 832b3562cd..3c0c6f0ac2 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -101,6 +101,10 @@ def result_headers(cl): "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}), "class_attrib": (th_classes and ' class="%s"' % ' '.join(th_classes) or '')} +def _boolean_icon(field_val): + BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} + return '%s' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val) + def items_for_result(cl, result): first = True pk = cl.lookup_opts.pk.attname @@ -114,9 +118,14 @@ def items_for_result(cl, result): try: attr = getattr(result, field_name) allow_tags = getattr(attr, 'allow_tags', False) + boolean = getattr(attr, 'boolean', False) if callable(attr): attr = attr() - result_repr = str(attr) + if boolean: + allow_tags = True + result_repr = _boolean_icon(attr) + else: + result_repr = str(attr) except (AttributeError, ObjectDoesNotExist): result_repr = EMPTY_CHANGELIST_VALUE else: @@ -147,8 +156,7 @@ def items_for_result(cl, result): row_class = ' class="nowrap"' # Booleans are special: We use images. elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField): - BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} - result_repr = '%s' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val) + result_repr = _boolean_icon(field_val) # FloatFields are special: Zero-pad the decimals. elif isinstance(f, models.FloatField): if field_val is not None: diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index a95748a9a1..3384134cb2 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +CONTENT_TYPE_CACHE = {} class ContentTypeManager(models.Manager): def get_for_model(self, model): """ @@ -8,10 +9,15 @@ class ContentTypeManager(models.Manager): ContentType if necessary. """ opts = model._meta - # The str() is needed around opts.verbose_name because it's a - # django.utils.functional.__proxy__ object. - ct, created = self.model._default_manager.get_or_create(app_label=opts.app_label, - model=opts.object_name.lower(), defaults={'name': str(opts.verbose_name)}) + key = (opts.app_label, opts.object_name.lower()) + try: + ct = CONTENT_TYPE_CACHE[key] + except KeyError: + # The str() is needed around opts.verbose_name because it's a + # django.utils.functional.__proxy__ object. + ct, created = self.model._default_manager.get_or_create(app_label=key[0], + model=key[1], defaults={'name': str(opts.verbose_name)}) + CONTENT_TYPE_CACHE[key] = ct return ct class ContentType(models.Model): diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py index 2337ad8a61..728caa7e19 100644 --- a/django/contrib/sessions/middleware.py +++ b/django/contrib/sessions/middleware.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.sessions.models import Session +from django.core.exceptions import SuspiciousOperation from django.utils.cache import patch_vary_headers import datetime @@ -55,7 +56,7 @@ class SessionWrapper(object): s = Session.objects.get(session_key=self.session_key, expire_date__gt=datetime.datetime.now()) self._session_cache = s.get_decoded() - except Session.DoesNotExist: + except (Session.DoesNotExist, SuspiciousOperation): self._session_cache = {} # Set the session_key to None to force creation of a new # key, for extra security. diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py index c68f33616c..4c64161538 100644 --- a/django/core/cache/backends/dummy.py +++ b/django/core/cache/backends/dummy.py @@ -6,8 +6,8 @@ class CacheClass(BaseCache): def __init__(self, *args, **kwargs): pass - def get(self, *args, **kwargs): - pass + def get(self, key, default=None): + return default def set(self, *args, **kwargs): pass @@ -16,7 +16,7 @@ class CacheClass(BaseCache): pass def get_many(self, *args, **kwargs): - pass + return {} def has_key(self, *args, **kwargs): return False diff --git a/django/core/management.py b/django/core/management.py index c0b3d3a15a..10d2532263 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -25,7 +25,7 @@ APP_ARGS = '[appname ...]' # which has been installed. PROJECT_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf', '%s_template') -INVALID_PROJECT_NAMES = ('django', 'test') +INVALID_PROJECT_NAMES = ('django', 'site', 'test') # Set up the terminal color scheme. class dummy: pass @@ -735,7 +735,7 @@ def startproject(project_name, directory): "Creates a Django project for the given project_name in the given directory." from random import choice if project_name in INVALID_PROJECT_NAMES: - sys.stderr.write(style.ERROR("Error: %r isn't a valid project name. Please try another.\n" % project_name)) + sys.stderr.write(style.ERROR("Error: '%r' conflicts with the name of an existing Python module and cannot be used as a project name. Please try another name.\n" % project_name)) sys.exit(1) _start_helper('project', project_name, directory) # Create a random SECRET_KEY hash, and put it in the main settings. diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 859816c226..1e1e6f4bec 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -57,7 +57,7 @@ def Deserializer(object_list, **options): for d in object_list: # Look up the model and starting build a dict of data for it. Model = _get_model(d["model"]) - data = {Model._meta.pk.name : d["pk"]} + data = {Model._meta.pk.attname : d["pk"]} m2m_data = {} # Handle each field diff --git a/django/db/backends/ado_mssql/creation.py b/django/db/backends/ado_mssql/creation.py index 4d85d27ea5..5158ba02f9 100644 --- a/django/db/backends/ado_mssql/creation.py +++ b/django/db/backends/ado_mssql/creation.py @@ -21,6 +21,5 @@ DATA_TYPES = { 'SmallIntegerField': 'smallint', 'TextField': 'text', 'TimeField': 'time', - 'URLField': 'varchar(200)', 'USStateField': 'varchar(2)', } diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index 116b490124..22ed901653 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -25,6 +25,5 @@ DATA_TYPES = { 'SmallIntegerField': 'smallint', 'TextField': 'longtext', 'TimeField': 'time', - 'URLField': 'varchar(200)', 'USStateField': 'varchar(2)', } diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index 65a804ec40..6c130f368e 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -25,6 +25,5 @@ DATA_TYPES = { 'SmallIntegerField': 'smallint', 'TextField': 'text', 'TimeField': 'time', - 'URLField': 'varchar(200)', 'USStateField': 'varchar(2)', } diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index e845179e64..77f570b2e8 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -24,6 +24,5 @@ DATA_TYPES = { 'SmallIntegerField': 'smallint', 'TextField': 'text', 'TimeField': 'time', - 'URLField': 'varchar(200)', 'USStateField': 'varchar(2)', } diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 2471514e0e..e5c9ff2fe2 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -337,11 +337,11 @@ class Field(object): return self._choices choices = property(_get_choices) - def formfield(self, initial=None): + def formfield(self, **kwargs): "Returns a django.newforms.Field instance for this database Field." - from django.newforms import CharField - # TODO: This is just a temporary default during development. - return forms.CharField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.CharField(**defaults) def value_from_object(self, obj): "Returns the value of this field in the given model instance." @@ -383,7 +383,7 @@ class AutoField(Field): super(AutoField, self).contribute_to_class(cls, name) cls._meta.has_auto_field = True - def formfield(self, initial=None): + def formfield(self, **kwargs): return None class BooleanField(Field): @@ -400,8 +400,10 @@ class BooleanField(Field): def get_manipulator_field_objs(self): return [oldforms.CheckboxField] - def formfield(self, initial=None): - return forms.BooleanField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.BooleanField(**defaults) class CharField(Field): def get_manipulator_field_objs(self): @@ -417,8 +419,10 @@ class CharField(Field): raise validators.ValidationError, gettext_lazy("This field cannot be null.") return str(value) - def formfield(self, initial=None): - return forms.CharField(max_length=self.maxlength, required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'max_length': self.maxlength, 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.CharField(**defaults) # TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): @@ -497,8 +501,10 @@ class DateField(Field): val = self._get_val_from_obj(obj) return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')} - def formfield(self, initial=None): - return forms.DateField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.DateField(**defaults) class DateTimeField(DateField): def to_python(self, value): @@ -569,8 +575,10 @@ class DateTimeField(DateField): return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), time_field: (val is not None and val.strftime("%H:%M:%S") or '')} - def formfield(self, initial=None): - return forms.DateTimeField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.DateTimeField(**defaults) class EmailField(CharField): def __init__(self, *args, **kwargs): @@ -586,8 +594,10 @@ class EmailField(CharField): def validate(self, field_data, all_data): validators.isValidEmail(field_data, all_data) - def formfield(self, initial=None): - return forms.EmailField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.EmailField(**defaults) class FileField(Field): def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs): @@ -721,8 +731,10 @@ class IntegerField(Field): def get_manipulator_field_objs(self): return [oldforms.IntegerField] - def formfield(self, initial=None): - return forms.IntegerField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.IntegerField(**defaults) class IPAddressField(Field): def __init__(self, *args, **kwargs): @@ -778,6 +790,11 @@ class TextField(Field): def get_manipulator_field_objs(self): return [oldforms.LargeTextField] + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'widget': forms.Textarea, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.CharField(**defaults) + class TimeField(Field): empty_strings_allowed = False def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): @@ -824,21 +841,29 @@ class TimeField(Field): val = self._get_val_from_obj(obj) return {self.attname: (val is not None and val.strftime("%H:%M:%S") or '')} - def formfield(self, initial=None): - return forms.TimeField(required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.TimeField(**defaults) -class URLField(Field): +class URLField(CharField): def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): + kwargs['maxlength'] = kwargs.get('maxlength', 200) if verify_exists: kwargs.setdefault('validator_list', []).append(validators.isExistingURL) self.verify_exists = verify_exists - Field.__init__(self, verbose_name, name, **kwargs) + CharField.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): return [oldforms.URLField] - def formfield(self, initial=None): - return forms.URLField(required=not self.blank, verify_exists=self.verify_exists, label=capfirst(self.verbose_name), initial=initial) + def get_internal_type(self): + return "CharField" + + def formfield(self, **kwargs): + defaults = {'required': not self.blank, 'verify_exists': self.verify_exists, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.URLField(**defaults) class USStateField(Field): def get_manipulator_field_objs(self): diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index e51c779550..b517747735 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -316,18 +316,20 @@ def create_many_related_manager(superclass): # join_table: name of the m2m link table # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object - # *objs - objects to add + # *objs - objects to add. Either object instances, or primary keys of object instances. from django.db import connection # If there aren't any objects, there is nothing to do. if objs: # Check that all the objects are of the right type + new_ids = set() for obj in objs: - if not isinstance(obj, self.model): - raise ValueError, "objects to add() must be %s instances" % self.model._meta.object_name + if isinstance(obj, self.model): + new_ids.add(obj._get_pk_val()) + else: + new_ids.add(obj) # Add the newly created or already existing objects to the join table. # First find out which items are already added, to avoid adding them twice - new_ids = set([obj._get_pk_val() for obj in objs]) cursor = connection.cursor() cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ (target_col_name, self.join_table, source_col_name, @@ -354,11 +356,13 @@ def create_many_related_manager(superclass): # If there aren't any objects, there is nothing to do. if objs: # Check that all the objects are of the right type + old_ids = set() for obj in objs: - if not isinstance(obj, self.model): - raise ValueError, "objects to remove() must be %s instances" % self.model._meta.object_name + if isinstance(obj, self.model): + old_ids.add(obj._get_pk_val()) + else: + old_ids.add(obj) # Remove the specified objects from the join table - old_ids = set([obj._get_pk_val() for obj in objs]) cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ (self.join_table, source_col_name, @@ -548,8 +552,10 @@ class ForeignKey(RelatedField, Field): def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) - def formfield(self, initial=None): - return forms.ChoiceField(choices=self.get_choices_default(), required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'choices': self.get_choices_default(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.ChoiceField(**defaults) class OneToOneField(RelatedField, IntegerField): def __init__(self, to, to_field=None, **kwargs): @@ -612,8 +618,10 @@ class OneToOneField(RelatedField, IntegerField): if not cls._meta.one_to_one_field: cls._meta.one_to_one_field = self - def formfield(self, initial=None): - return forms.ChoiceField(choices=self.get_choices_default(), required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + def formfield(self, **kwargs): + defaults = {'choices': self.get_choices_default(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.ChoiceField(**kwargs) class ManyToManyField(RelatedField, Field): def __init__(self, to, **kwargs): @@ -625,6 +633,7 @@ class ManyToManyField(RelatedField, Field): limit_choices_to=kwargs.pop('limit_choices_to', None), raw_id_admin=kwargs.pop('raw_id_admin', False), symmetrical=kwargs.pop('symmetrical', True)) + self.db_table = kwargs.pop('db_table', None) if kwargs["rel"].raw_id_admin: kwargs.setdefault("validator_list", []).append(self.isValidIDList) Field.__init__(self, **kwargs) @@ -647,10 +656,10 @@ class ManyToManyField(RelatedField, Field): def _get_m2m_db_table(self, opts): "Function that can be curried to provide the m2m table name for this relation" - from django.db import backend - from django.db.backends.util import truncate_name - name = '%s_%s' % (opts.db_table, self.name) - return truncate_name(name, backend.get_max_name_length()) + if self.db_table: + return self.db_table + else: + return '%s_%s' % (opts.db_table, self.name) def _get_m2m_column_name(self, related): "Function that can be curried to provide the source column name for the m2m table" @@ -728,12 +737,14 @@ class ManyToManyField(RelatedField, Field): "Returns the value of this field in the given model instance." return getattr(obj, self.attname).all() - def formfield(self, initial=None): + def formfield(self, **kwargs): # If initial is passed in, it's a list of related objects, but the # MultipleChoiceField takes a list of IDs. - if initial is not None: - initial = [i._get_pk_val() for i in initial] - return forms.MultipleChoiceField(choices=self.get_choices_default(), required=not self.blank, label=capfirst(self.verbose_name), initial=initial) + if kwargs.get('initial') is not None: + kwargs['initial'] = [i._get_pk_val() for i in kwargs['initial']] + defaults = {'choices': self.get_choices_default(), 'required': not self.blank, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} + defaults.update(kwargs) + return forms.MultipleChoiceField(**defaults) class ManyToOneRel(object): def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 6005874516..b60eed262a 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,4 +1,4 @@ -from django.db.models.query import QuerySet +from django.db.models.query import QuerySet, EmptyQuerySet from django.dispatch import dispatcher from django.db.models import signals from django.db.models.fields import FieldDoesNotExist @@ -41,12 +41,18 @@ class Manager(object): ####################### # PROXIES TO QUERYSET # ####################### + + def get_empty_query_set(self): + return EmptyQuerySet(self.model) def get_query_set(self): """Returns a new QuerySet object. Subclasses can override this method to easily customise the behaviour of the Manager. """ return QuerySet(self.model) + + def none(self): + return self.get_empty_query_set() def all(self): return self.get_query_set() diff --git a/django/db/models/query.py b/django/db/models/query.py index 231c5c12b0..a124da640b 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,5 +1,6 @@ from django.db import backend, connection, get_query_module, transaction from django.db.models.fields import DateField, FieldDoesNotExist +from django.db.models.fields.generic import GenericRelation from django.db.models import signals from django.dispatch import dispatcher from django.utils.datastructures import SortedDict @@ -25,6 +26,9 @@ QUERY_TERMS = ( # Larger values are slightly faster at the expense of more storage space. GET_ITERATOR_CHUNK_SIZE = 100 +class EmptyResultSet(Exception): + pass + #################### # HELPER FUNCTIONS # #################### @@ -169,7 +173,11 @@ class _QuerySet(object): cursor = connection.cursor() - select, sql, params, full_query = self._get_sql_clause() + try: + select, sql, params, full_query = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) fill_cache = self._select_related @@ -194,7 +202,12 @@ class _QuerySet(object): counter._offset = None counter._limit = None counter._select_related = False - select, sql, params, full_query = counter._get_sql_clause() + + try: + select, sql, params, full_query = counter._get_sql_clause() + except EmptyResultSet: + return 0 + cursor = connection.cursor() if self._distinct: id_col = "%s.%s" % (backend.quote_name(self.model._meta.db_table), @@ -532,7 +545,12 @@ class ValuesQuerySet(QuerySet): field_names = [f.attname for f in self.model._meta.fields] cursor = connection.cursor() - select, sql, params, full_query = self._get_sql_clause() + + try: + select, sql, params, full_query = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) while 1: @@ -554,19 +572,25 @@ class DateQuerySet(QuerySet): if self._field.null: self._where.append('%s.%s IS NOT NULL' % \ (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column))) - select, sql, params, full_query = self._get_sql_clause() + try: + select, sql, params, full_query = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + table_name = backend.quote_name(self.model._meta.db_table) field_name = backend.quote_name(self._field.column) - date_trunc_sql = backend.get_date_trunc_sql(self._kind, - '%s.%s' % (table_name, field_name)) + if backend.allows_group_by_ordinal: group_by = '1' else: - group_by = date_trunc_sql - fmt = 'SELECT %s %s GROUP BY %s ORDER BY 1 %s' - stmt = fmt % (date_trunc_sql, sql, group_by, self._order) + group_by = backend.get_date_trunc_sql(self._kind, + '%s.%s' % (table_name, field_name)) + + sql = 'SELECT %s %s GROUP BY %s ORDER BY 1 %s' % \ + (backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table), + backend.quote_name(self._field.column))), sql, group_by, self._order) cursor = connection.cursor() - cursor.execute(stmt, params) + cursor.execute(sql, params) if backend.needs_datetime_string_cast: return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()] else: @@ -579,6 +603,25 @@ class DateQuerySet(QuerySet): c._order = self._order return c +class EmptyQuerySet(QuerySet): + def __init__(self, model=None): + super(EmptyQuerySet, self).__init__(model) + self._result_cache = [] + + def iterator(self): + raise StopIteration + + def count(self): + return 0 + + def delete(self): + pass + + def _clone(self, klass=None, **kwargs): + c = super(EmptyQuerySet, self)._clone(klass, **kwargs) + c._result_cache = [] + return c + class QOperator(object): "Base class for QAnd and QOr" def __init__(self, *args): @@ -587,10 +630,14 @@ class QOperator(object): def get_sql(self, opts): joins, where, params = SortedDict(), [], [] for val in self.args: - joins2, where2, params2 = val.get_sql(opts) - joins.update(joins2) - where.extend(where2) - params.extend(params2) + try: + joins2, where2, params2 = val.get_sql(opts) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + except EmptyResultSet: + if not isinstance(self, QOr): + raise EmptyResultSet if where: return joins, ['(%s)' % self.operator.join(where)], params return joins, [], params @@ -644,8 +691,11 @@ class QNot(Q): self.q = q def get_sql(self, opts): - joins, where, params = self.q.get_sql(opts) - where2 = ['(NOT (%s))' % " AND ".join(where)] + try: + joins, where, params = self.q.get_sql(opts) + where2 = ['(NOT (%s))' % " AND ".join(where)] + except EmptyResultSet: + return SortedDict(), [], [] return joins, where2, params def get_where_clause(lookup_type, table_prefix, field_name, value): @@ -662,7 +712,11 @@ def get_where_clause(lookup_type, table_prefix, field_name, value): except KeyError: pass if lookup_type == 'in': - return '%s%s IN (%s)' % (table_prefix, field_name, ','.join(['%s' for v in value])) + in_string = ','.join(['%s' for id in value]) + if in_string: + return '%s%s IN (%s)' % (table_prefix, field_name, in_string) + else: + raise EmptyResultSet elif lookup_type == 'range': return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name) elif lookup_type in ('year', 'month', 'day'): @@ -948,18 +1002,26 @@ def delete_objects(seen_objs): pk_list = [pk for pk,instance in seen_objs[cls]] for related in cls._meta.get_all_related_many_to_many_objects(): - for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ - (qn(related.field.m2m_db_table()), - qn(related.field.m2m_reverse_name()), - ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), - pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + if not isinstance(related.field, GenericRelation): + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ + (qn(related.field.m2m_db_table()), + qn(related.field.m2m_reverse_name()), + ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), + pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) for f in cls._meta.many_to_many: + if isinstance(f, GenericRelation): + from django.contrib.contenttypes.models import ContentType + query_extra = 'AND %s=%%s' % f.rel.to._meta.get_field(f.content_type_field_name).column + args_extra = [ContentType.objects.get_for_model(cls).id] + else: + query_extra = '' + args_extra = [] for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ + cursor.execute(("DELETE FROM %s WHERE %s IN (%s)" % \ (qn(f.m2m_db_table()), qn(f.m2m_column_name()), - ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), - pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]]))) + query_extra, + pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE] + args_extra) for field in cls._meta.fields: if field.rel and field.null and field.rel.to in seen_objs: for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 408c90df45..e9e1fb7746 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -3,8 +3,8 @@ Field classes """ from django.utils.translation import gettext -from util import ValidationError, smart_unicode -from widgets import TextInput, PasswordInput, CheckboxInput, Select, SelectMultiple +from util import ErrorList, ValidationError, smart_unicode +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple import datetime import re import time @@ -15,8 +15,9 @@ __all__ = ( 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', 'RegexField', 'EmailField', 'URLField', 'BooleanField', - 'ChoiceField', 'MultipleChoiceField', - 'ComboField', + 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', + 'ComboField', 'MultiValueField', + 'SplitDateTimeField', ) # These values, if given to to_python(), will trigger the self.required check. @@ -29,11 +30,12 @@ except NameError: class Field(object): widget = TextInput # Default widget to use when rendering this type of Field. + hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". # Tracks each time a Field instance is created. Used to retain order. creation_counter = 0 - def __init__(self, required=True, widget=None, label=None, initial=None): + def __init__(self, required=True, widget=None, label=None, initial=None, help_text=None): # required -- Boolean that specifies whether the field is required. # True by default. # widget -- A Widget class, or instance of a Widget class, that should be @@ -45,9 +47,11 @@ class Field(object): # field name, if the Field is part of a Form. # initial -- A value to use in this Field's initial display. This value is # *not* used as a fallback if data isn't given. + # help_text -- An optional string to use as "help text" for this Field. if label is not None: label = smart_unicode(label) self.required, self.label, self.initial = required, label, initial + self.help_text = help_text widget = widget or self.widget if isinstance(widget, type): widget = widget() @@ -83,17 +87,15 @@ class Field(object): return {} class CharField(Field): - def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None, initial=None): + def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length - Field.__init__(self, required, widget, label, initial) + super(CharField, self).__init__(*args, **kwargs) def clean(self, value): "Validates max_length and min_length. Returns a Unicode object." - Field.clean(self, value) + super(CharField, self).clean(value) if value in EMPTY_VALUES: - value = u'' - if not self.required: - return value + return u'' value = smart_unicode(value) if self.max_length is not None and len(value) > self.max_length: raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) @@ -106,18 +108,18 @@ class CharField(Field): return {'maxlength': str(self.max_length)} class IntegerField(Field): - def __init__(self, max_value=None, min_value=None, required=True, widget=None, label=None, initial=None): + def __init__(self, max_value=None, min_value=None, *args, **kwargs): self.max_value, self.min_value = max_value, min_value - Field.__init__(self, required, widget, label, initial) + super(IntegerField, self).__init__(*args, **kwargs) def clean(self, value): """ Validates that int() can be called on the input. Returns the result - of int(). + of int(). Returns None for empty values. """ super(IntegerField, self).clean(value) - if not self.required and value in EMPTY_VALUES: - return u'' + if value in EMPTY_VALUES: + return None try: value = int(value) except (ValueError, TypeError): @@ -137,8 +139,8 @@ DEFAULT_DATE_INPUT_FORMATS = ( ) class DateField(Field): - def __init__(self, input_formats=None, required=True, widget=None, label=None, initial=None): - Field.__init__(self, required, widget, label, initial) + def __init__(self, input_formats=None, *args, **kwargs): + super(DateField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS def clean(self, value): @@ -146,7 +148,7 @@ class DateField(Field): Validates that the input can be converted to a date. Returns a Python datetime.date object. """ - Field.clean(self, value) + super(DateField, self).clean(value) if value in EMPTY_VALUES: return None if isinstance(value, datetime.datetime): @@ -166,8 +168,8 @@ DEFAULT_TIME_INPUT_FORMATS = ( ) class TimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None, label=None, initial=None): - Field.__init__(self, required, widget, label, initial) + def __init__(self, input_formats=None, *args, **kwargs): + super(TimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS def clean(self, value): @@ -175,7 +177,7 @@ class TimeField(Field): Validates that the input can be converted to a time. Returns a Python datetime.time object. """ - Field.clean(self, value) + super(TimeField, self).clean(value) if value in EMPTY_VALUES: return None if isinstance(value, datetime.time): @@ -200,8 +202,8 @@ DEFAULT_DATETIME_INPUT_FORMATS = ( ) class DateTimeField(Field): - def __init__(self, input_formats=None, required=True, widget=None, label=None, initial=None): - Field.__init__(self, required, widget, label, initial) + def __init__(self, input_formats=None, *args, **kwargs): + super(DateTimeField, self).__init__(*args, **kwargs) self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS def clean(self, value): @@ -209,7 +211,7 @@ class DateTimeField(Field): Validates that the input can be converted to a datetime. Returns a Python datetime.datetime object. """ - Field.clean(self, value) + super(DateTimeField, self).clean(value) if value in EMPTY_VALUES: return None if isinstance(value, datetime.datetime): @@ -224,14 +226,13 @@ class DateTimeField(Field): raise ValidationError(gettext(u'Enter a valid date/time.')) class RegexField(Field): - def __init__(self, regex, max_length=None, min_length=None, error_message=None, - required=True, widget=None, label=None, initial=None): + def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): """ regex can be either a string or a compiled regular expression object. error_message is an optional error message to use, if 'Enter a valid value' is too generic for you. """ - Field.__init__(self, required, widget, label, initial) + super(RegexField, self).__init__(*args, **kwargs) if isinstance(regex, basestring): regex = re.compile(regex) self.regex = regex @@ -243,10 +244,11 @@ class RegexField(Field): Validates that the input matches the regular expression. Returns a Unicode object. """ - Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + super(RegexField, self).clean(value) + if value in EMPTY_VALUES: + value = u'' value = smart_unicode(value) - if not self.required and value == u'': + if value == u'': return value if self.max_length is not None and len(value) > self.max_length: raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) @@ -262,8 +264,9 @@ email_re = re.compile( r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain class EmailField(RegexField): - def __init__(self, max_length=None, min_length=None, required=True, widget=None, label=None, initial=None): - RegexField.__init__(self, email_re, max_length, min_length, gettext(u'Enter a valid e-mail address.'), required, widget, label, initial) + def __init__(self, max_length=None, min_length=None, *args, **kwargs): + RegexField.__init__(self, email_re, max_length, min_length, + gettext(u'Enter a valid e-mail address.'), *args, **kwargs) url_re = re.compile( r'^https?://' # http:// or https:// @@ -279,14 +282,16 @@ except ImportError: URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' class URLField(RegexField): - def __init__(self, max_length=None, min_length=None, required=True, verify_exists=False, widget=None, label=None, - initial=None, validator_user_agent=URL_VALIDATOR_USER_AGENT): - RegexField.__init__(self, url_re, max_length, min_length, gettext(u'Enter a valid URL.'), required, widget, label, initial) + def __init__(self, max_length=None, min_length=None, verify_exists=False, + validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): + super(URLField, self).__init__(url_re, max_length, min_length, gettext(u'Enter a valid URL.'), *args, **kwargs) self.verify_exists = verify_exists self.user_agent = validator_user_agent def clean(self, value): - value = RegexField.clean(self, value) + value = super(URLField, self).clean(value) + if value == u'': + return value if self.verify_exists: import urllib2 from django.conf import settings @@ -311,24 +316,43 @@ class BooleanField(Field): def clean(self, value): "Returns a Python boolean object." - Field.clean(self, value) + super(BooleanField, self).clean(value) return bool(value) +class NullBooleanField(BooleanField): + """ + A field whose valid values are None, True and False. Invalid values are + cleaned to None. + """ + widget = NullBooleanSelect + + def clean(self, value): + return {True: True, False: False}.get(value, None) + class ChoiceField(Field): - def __init__(self, choices=(), required=True, widget=Select, label=None, initial=None): - if isinstance(widget, type): - widget = widget(choices=choices) - Field.__init__(self, required, widget, label, initial) + def __init__(self, choices=(), required=True, widget=Select, label=None, initial=None, help_text=None): + super(ChoiceField, self).__init__(required, widget, label, initial, help_text) self.choices = choices + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + self._choices = value + self.widget.choices = value + + choices = property(_get_choices, _set_choices) + def clean(self, value): """ Validates that the input is in self.choices. """ - value = Field.clean(self, value) - if value in EMPTY_VALUES: value = u'' + value = super(ChoiceField, self).clean(value) + if value in EMPTY_VALUES: + value = u'' value = smart_unicode(value) - if not self.required and value == u'': + if value == u'': return value valid_values = set([str(k) for k, v in self.choices]) if value not in valid_values: @@ -336,8 +360,10 @@ class ChoiceField(Field): return value class MultipleChoiceField(ChoiceField): - def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None, initial=None): - ChoiceField.__init__(self, choices, required, widget, label, initial) + hidden_widget = MultipleHiddenInput + + def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None, initial=None, help_text=None): + super(MultipleChoiceField, self).__init__(choices, required, widget, label, initial, help_text) def clean(self, value): """ @@ -361,8 +387,11 @@ class MultipleChoiceField(ChoiceField): return new_value class ComboField(Field): - def __init__(self, fields=(), required=True, widget=None, label=None, initial=None): - Field.__init__(self, required, widget, label, initial) + """ + A Field whose clean() method calls multiple Field clean() methods. + """ + def __init__(self, fields=(), *args, **kwargs): + super(ComboField, self).__init__(*args, **kwargs) # Set 'required' to False on the individual fields, because the # required validation will be handled by ComboField, not by those # individual fields. @@ -375,7 +404,88 @@ class ComboField(Field): Validates the given value against all of self.fields, which is a list of Field instances. """ - Field.clean(self, value) + super(ComboField, self).clean(value) for field in self.fields: value = field.clean(value) return value + +class MultiValueField(Field): + """ + A Field that is composed of multiple Fields. + + Its clean() method takes a "decompressed" list of values. Each value in + this list is cleaned by the corresponding field -- the first value is + cleaned by the first field, the second value is cleaned by the second + field, etc. Once all fields are cleaned, the list of clean values is + "compressed" into a single value. + + Subclasses should implement compress(), which specifies how a list of + valid values should be converted to a single value. Subclasses should not + have to implement clean(). + + You'll probably want to use this with MultiWidget. + """ + def __init__(self, fields=(), *args, **kwargs): + super(MultiValueField, self).__init__(*args, **kwargs) + # Set 'required' to False on the individual fields, because the + # required validation will be handled by MultiValueField, not by those + # individual fields. + for f in fields: + f.required = False + self.fields = fields + + def clean(self, value): + """ + Validates every value in the given list. A value is validated against + the corresponding Field in self.fields. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), clean() would call + DateField.clean(value[0]) and TimeField.clean(value[1]). + """ + clean_data = [] + errors = ErrorList() + if self.required and not value: + raise ValidationError(gettext(u'This field is required.')) + elif not self.required and not value: + return self.compress([]) + if not isinstance(value, (list, tuple)): + raise ValidationError(gettext(u'Enter a list of values.')) + for i, field in enumerate(self.fields): + try: + field_value = value[i] + except KeyError: + field_value = None + if self.required and field_value in EMPTY_VALUES: + raise ValidationError(gettext(u'This field is required.')) + try: + clean_data.append(field.clean(field_value)) + except ValidationError, e: + # Collect all validation errors in a single list, which we'll + # raise at the end of clean(), rather than raising a single + # exception for the first error we encounter. + errors.extend(e.messages) + if errors: + raise ValidationError(errors) + return self.compress(clean_data) + + def compress(self, data_list): + """ + Returns a single value for the given list of values. The values can be + assumed to be valid. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), this might return a datetime + object created by combining the date and time in data_list. + """ + raise NotImplementedError('Subclasses must implement this method.') + +class SplitDateTimeField(MultiValueField): + def __init__(self, *args, **kwargs): + fields = (DateField(), TimeField()) + super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + return datetime.datetime.combine(*data_list) + return None diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 4928065e9d..7e29941bef 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -5,8 +5,8 @@ Form classes from django.utils.datastructures import SortedDict, MultiValueDict from django.utils.html import escape from fields import Field -from widgets import TextInput, Textarea, HiddenInput -from util import StrAndUnicode, ErrorDict, ErrorList, ValidationError +from widgets import TextInput, Textarea, HiddenInput, MultipleHiddenInput +from util import flatatt, StrAndUnicode, ErrorDict, ErrorList, ValidationError __all__ = ('BaseForm', 'Form') @@ -26,12 +26,15 @@ class SortedDictFromList(SortedDict): self.keyOrder = [d[0] for d in data] dict.__init__(self, dict(data)) + def copy(self): + return SortedDictFromList(self.items()) + class DeclarativeFieldsMetaclass(type): - "Metaclass that converts Field attributes to a dictionary called 'fields'." + "Metaclass that converts Field attributes to a dictionary called 'base_fields'." def __new__(cls, name, bases, attrs): - fields = [(name, attrs.pop(name)) for name, obj in attrs.items() if isinstance(obj, Field)] + fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) - attrs['fields'] = SortedDictFromList(fields) + attrs['base_fields'] = SortedDictFromList(fields) return type.__new__(cls, name, bases, attrs) class BaseForm(StrAndUnicode): @@ -39,13 +42,19 @@ class BaseForm(StrAndUnicode): # class is different than Form. See the comments by the Form class for more # information. Any improvements to the form API should be made to *this* # class, not to the Form class. - def __init__(self, data=None, auto_id='id_%s', prefix=None): - self.ignore_errors = data is None + def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None): + self.is_bound = data is not None self.data = data or {} self.auto_id = auto_id self.prefix = prefix - self.clean_data = None # Stores the data after clean() has been called. + self.initial = initial or {} self.__errors = None # Stores the errors after clean() has been called. + # The base_fields class attribute is the *class-wide* definition of + # fields. Because a particular *instance* of the class might want to + # alter self.fields, we create self.fields here by copying base_fields. + # Instances should always modify self.fields; they should not modify + # self.base_fields. + self.fields = self.base_fields.copy() def __unicode__(self): return self.as_table() @@ -74,7 +83,7 @@ class BaseForm(StrAndUnicode): Returns True if the form has no errors. Otherwise, False. If errors are being ignored, returns False. """ - return not self.ignore_errors and not bool(self.errors) + return self.is_bound and not bool(self.errors) def add_prefix(self, field_name): """ @@ -85,7 +94,7 @@ class BaseForm(StrAndUnicode): """ return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name - def _html_output(self, normal_row, error_row, row_ender, errors_on_separate_row): + def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." top_errors = self.non_field_errors() # Errors that should be displayed above all fields. output, hidden_fields = [], [] @@ -100,7 +109,11 @@ class BaseForm(StrAndUnicode): if errors_on_separate_row and bf_errors: output.append(error_row % bf_errors) label = bf.label and bf.label_tag(escape(bf.label + ':')) or '' - output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': bf}) + if field.help_text: + help_text = help_text_html % field.help_text + else: + help_text = u'' + output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': unicode(bf), 'help_text': help_text}) if top_errors: output.insert(0, error_row % top_errors) if hidden_fields: # Insert any hidden fields in the last row. @@ -115,15 +128,15 @@ class BaseForm(StrAndUnicode): def as_table(self): "Returns this form rendered as HTML s -- excluding the
." - return self._html_output(u'%(label)s%(errors)s%(field)s', u'%s', '', False) + return self._html_output(u'%(label)s%(errors)s%(field)s%(help_text)s', u'%s', '', u'
%s', False) def as_ul(self): "Returns this form rendered as HTML
  • s -- excluding the ." - return self._html_output(u'
  • %(errors)s%(label)s %(field)s
  • ', u'
  • %s
  • ', '', False) + return self._html_output(u'
  • %(errors)s%(label)s %(field)s%(help_text)s
  • ', u'
  • %s
  • ', '', u' %s', False) def as_p(self): "Returns this form rendered as HTML

    s." - return self._html_output(u'

    %(label)s %(field)s

    ', u'

    %s

    ', '

    ', True) + return self._html_output(u'

    %(label)s %(field)s%(help_text)s

    ', u'

    %s

    ', '

    ', u' %s', True) def non_field_errors(self): """ @@ -137,11 +150,11 @@ class BaseForm(StrAndUnicode): """ Cleans all of self.data and populates self.__errors and self.clean_data. """ - self.clean_data = {} errors = ErrorDict() - if self.ignore_errors: # Stop further processing. + if not self.is_bound: # Stop further processing. self.__errors = errors return + self.clean_data = {} for name, field in self.fields.items(): # value_from_datadict() gets the data from the dictionary. # Each widget type knows how to retrieve its own data, because some @@ -160,7 +173,7 @@ class BaseForm(StrAndUnicode): except ValidationError, e: errors[NON_FIELD_ERRORS] = e.messages if errors: - self.clean_data = None + delattr(self, 'clean_data') self.__errors = errors def clean(self): @@ -218,8 +231,8 @@ class BoundField(StrAndUnicode): auto_id = self.auto_id if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): attrs['id'] = auto_id - if self.form.ignore_errors: - data = self.field.initial + if not self.form.is_bound: + data = self.form.initial.get(self.name, self.field.initial) else: data = self.data return widget.render(self.html_name, data, attrs=attrs) @@ -238,7 +251,7 @@ class BoundField(StrAndUnicode): """ Returns a string of HTML for representing this as an . """ - return self.as_widget(HiddenInput(), attrs) + return self.as_widget(self.field.hidden_widget(), attrs) def _data(self): """ @@ -247,17 +260,20 @@ class BoundField(StrAndUnicode): return self.field.widget.value_from_datadict(self.form.data, self.html_name) data = property(_data) - def label_tag(self, contents=None): + def label_tag(self, contents=None, attrs=None): """ Wraps the given contents in a