From 7f13278f8619b1155fa51276bb63afa9997610da Mon Sep 17 00:00:00 2001 From: Boulder Sprinters Date: Tue, 8 May 2007 17:46:05 +0000 Subject: boulder-oracle-sprint: Merged to [5173] git-svn-id: http://code.djangoproject.com/svn/django/branches/boulder-oracle-sprint@5174 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 5 +- django/conf/locale/de/LC_MESSAGES/django.mo | Bin 44060 -> 44063 bytes django/conf/locale/de/LC_MESSAGES/django.po | 16 +- django/contrib/admin/templatetags/log.py | 9 +- django/contrib/contenttypes/generic.py | 260 +++++++++++++++++++++ django/core/cache/backends/base.py | 3 + django/core/management.py | 4 +- django/db/models/__init__.py | 1 - django/db/models/base.py | 2 +- django/db/models/fields/generic.py | 260 --------------------- django/db/models/query.py | 9 +- django/template/__init__.py | 9 + django/test/testcases.py | 22 +- django/test/utils.py | 32 ++- docs/contributing.txt | 9 +- docs/i18n.txt | 6 +- docs/model-api.txt | 2 +- docs/serialization.txt | 2 +- docs/sitemaps.txt | 2 - docs/templates_python.txt | 21 +- docs/testing.txt | 104 +++++++-- tests/modeltests/generic_relations/models.py | 7 +- tests/modeltests/test_client/models.py | 34 +++ tests/modeltests/test_client/urls.py | 4 +- tests/modeltests/test_client/views.py | 26 +++ tests/regressiontests/cache/tests.py | 5 + .../regressiontests/serializers_regress/models.py | 5 +- tests/regressiontests/templates/tests.py | 13 +- 28 files changed, 531 insertions(+), 341 deletions(-) create mode 100644 django/contrib/contenttypes/generic.py delete mode 100644 django/db/models/fields/generic.py diff --git a/AUTHORS b/AUTHORS index 027dbc39ba..fb2d5f7112 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,12 +43,14 @@ answer newbie questions, and generally made Django that much better: adurdin@gmail.com alang@bright-green.com + Marty Alchin Daniel Alves Barbosa de Oliveira Vaz Andreas andy@jadedplanet.net Fabrice Aneche ant9000@netwise.it David Ascher + david@kazserve.org Arthur axiak@mit.edu Jiri Barton @@ -68,6 +70,7 @@ answer newbie questions, and generally made Django that much better: Amit Chakradeo ChaosKCW ivan.chelubeev@gmail.com + Bryan Chow Ian Clelland crankycoder@gmail.com Matt Croydon @@ -145,7 +148,7 @@ answer newbie questions, and generally made Django that much better: lerouxb@gmail.com Waylan Limberg limodou - mattmcc + Matt McClanahan Martin Maney masonsimon+django@gmail.com Manuzhai diff --git a/django/conf/locale/de/LC_MESSAGES/django.mo b/django/conf/locale/de/LC_MESSAGES/django.mo index 396fe5a432..5f2eee4f33 100644 Binary files a/django/conf/locale/de/LC_MESSAGES/django.mo and b/django/conf/locale/de/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/de/LC_MESSAGES/django.po b/django/conf/locale/de/LC_MESSAGES/django.po index 9237e18b2a..52b70bda00 100644 --- a/django/conf/locale/de/LC_MESSAGES/django.po +++ b/django/conf/locale/de/LC_MESSAGES/django.po @@ -367,11 +367,11 @@ msgstr "Abmelden" #: contrib/admin/templates/admin/base_site.html:4 msgid "Django site admin" -msgstr "Django Systemverwaltung" +msgstr "Django-Systemverwaltung" #: contrib/admin/templates/admin/base_site.html:7 msgid "Django administration" -msgstr "Django Verwaltung" +msgstr "Django-Verwaltung" #: contrib/admin/templates/admin/change_form.html:15 #: contrib/admin/templates/admin/index.html:28 @@ -385,7 +385,7 @@ msgstr "Geschichte" #: contrib/admin/templates/admin/change_form.html:22 msgid "View on site" -msgstr "Im Web Anzeigen" +msgstr "Im Web anzeigen" #: contrib/admin/templates/admin/change_form.html:32 #: contrib/admin/templates/admin/auth/user/change_password.html:24 @@ -614,7 +614,7 @@ msgid "" "your computer is \"internal\").

\n" msgstr "" "\n" -"

Um Bookmarklets zu installieren müssen diese Links in die\n" +"

Um Bookmarklets zu installieren, müssen diese Links in die\n" "Browser-Werkzeugleiste gezogen werden, oder mittels rechter Maustaste in " "die\n" "Bookmarks gespeichert werden. Danach können die Bookmarklets von jeder " @@ -998,7 +998,7 @@ msgstr "%s ist scheinbar kein urlpattern Objekt" #: contrib/admin/views/main.py:223 msgid "Site administration" -msgstr "Website Verwaltung" +msgstr "Website-Verwaltung" #: contrib/admin/views/main.py:271 contrib/admin/views/main.py:356 #, python-format @@ -1023,7 +1023,7 @@ msgstr "und" #: contrib/admin/views/main.py:337 #, python-format msgid "Changed %s." -msgstr "%s geändert" +msgstr "%s geändert." #: contrib/admin/views/main.py:339 #, python-format @@ -1490,8 +1490,8 @@ msgstr "Ihr Name:" msgid "" "This rating is required because you've entered at least one other rating." msgstr "" -"Diese Abstimmung ist zwingend erforderlich, da Du an mindestens einer " -"weiteren Abstimmung teilnimmst." +"Diese Abstimmung ist zwingend erforderlich, da Sie an mindestens einer " +"weiteren Abstimmung teilnehmen." #: contrib/comments/views/comments.py:111 #, python-format diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index 5caba2b795..8d52d2e944 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -11,9 +11,12 @@ class AdminLogNode(template.Node): return "" def render(self, context): - if self.user is not None and not self.user.isdigit(): - self.user = context[self.user].id - context[self.varname] = LogEntry.objects.filter(user__id__exact=self.user).select_related()[:self.limit] + if self.user is None: + context[self.varname] = LogEntry.objects.all().select_related()[:self.limit] + else: + if not self.user.isdigit(): + self.user = context[self.user].id + context[self.varname] = LogEntry.objects.filter(user__id__exact=self.user).select_related()[:self.limit] return '' class DoGetAdminLog: diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py new file mode 100644 index 0000000000..f995ab2044 --- /dev/null +++ b/django/contrib/contenttypes/generic.py @@ -0,0 +1,260 @@ +""" +Classes allowing "generic" relations through ContentType and object-id fields. +""" + +from django import oldforms +from django.core.exceptions import ObjectDoesNotExist +from django.db import backend +from django.db.models import signals +from django.db.models.fields.related import RelatedField, Field, ManyToManyRel +from django.db.models.loading import get_model +from django.dispatch import dispatcher +from django.utils.functional import curry + +class GenericForeignKey(object): + """ + Provides a generic relation to any object through content-type/object-id + fields. + """ + + def __init__(self, ct_field="content_type", fk_field="object_id"): + self.ct_field = ct_field + self.fk_field = fk_field + + def contribute_to_class(self, cls, name): + # Make sure the fields exist (these raise FieldDoesNotExist, + # which is a fine error to raise here) + self.name = name + self.model = cls + self.cache_attr = "_%s_cache" % name + + # For some reason I don't totally understand, using weakrefs here doesn't work. + dispatcher.connect(self.instance_pre_init, signal=signals.pre_init, sender=cls, weak=False) + + # Connect myself as the descriptor for this field + setattr(cls, name, self) + + def instance_pre_init(self, signal, sender, args, kwargs): + # Handle initalizing an object with the generic FK instaed of + # content-type/object-id fields. + if self.name in kwargs: + value = kwargs.pop(self.name) + kwargs[self.ct_field] = self.get_content_type(value) + kwargs[self.fk_field] = value._get_pk_val() + + def get_content_type(self, obj): + # Convenience function using get_model avoids a circular import when using this model + ContentType = get_model("contenttypes", "contenttype") + return ContentType.objects.get_for_model(obj) + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "%s must be accessed via instance" % self.name + + try: + return getattr(instance, self.cache_attr) + except AttributeError: + rel_obj = None + ct = getattr(instance, self.ct_field) + if ct: + try: + rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field)) + except ObjectDoesNotExist: + pass + setattr(instance, self.cache_attr, rel_obj) + return rel_obj + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name + + ct = None + fk = None + if value is not None: + ct = self.get_content_type(value) + fk = value._get_pk_val() + + setattr(instance, self.ct_field, ct) + setattr(instance, self.fk_field, fk) + setattr(instance, self.cache_attr, value) + +class GenericRelation(RelatedField, Field): + """Provides an accessor to generic related objects (i.e. comments)""" + + def __init__(self, to, **kwargs): + kwargs['verbose_name'] = kwargs.get('verbose_name', None) + kwargs['rel'] = GenericRel(to, + related_name=kwargs.pop('related_name', None), + limit_choices_to=kwargs.pop('limit_choices_to', None), + symmetrical=kwargs.pop('symmetrical', True)) + + # Override content-type/object-id field names on the related class + self.object_id_field_name = kwargs.pop("object_id_field", "object_id") + self.content_type_field_name = kwargs.pop("content_type_field", "content_type") + + kwargs['blank'] = True + kwargs['editable'] = False + kwargs['serialize'] = False + Field.__init__(self, **kwargs) + + def get_manipulator_field_objs(self): + choices = self.get_choices_default() + return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + + def get_choices_default(self): + return Field.get_choices(self, include_blank=False) + + def flatten_data(self, follow, obj = None): + new_data = {} + if obj: + instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()] + new_data[self.name] = instance_ids + return new_data + + def m2m_db_table(self): + return self.rel.to._meta.db_table + + def m2m_column_name(self): + return self.object_id_field_name + + def m2m_reverse_name(self): + return self.object_id_field_name + + def contribute_to_class(self, cls, name): + super(GenericRelation, self).contribute_to_class(cls, name) + + # Save a reference to which model this class is on for future use + self.model = cls + + # Add the descriptor for the m2m relation + setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self)) + + def contribute_to_related_class(self, cls, related): + pass + + def set_attributes_from_rel(self): + pass + + def get_internal_type(self): + return "ManyToManyField" + +class ReverseGenericRelatedObjectsDescriptor(object): + """ + This class provides the functionality that makes the related-object + managers available as attributes on a model class, for fields that have + multiple "remote" values and have a GenericRelation defined in their model + (rather than having another model pointed *at* them). In the example + "article.publications", the publications attribute is a + ReverseGenericRelatedObjectsDescriptor instance. + """ + def __init__(self, field): + self.field = field + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + # This import is done here to avoid circular import importing this module + from django.contrib.contenttypes.models import ContentType + + # Dynamically create a class that subclasses the related model's + # default manager. + rel_model = self.field.rel.to + superclass = rel_model._default_manager.__class__ + RelatedManager = create_generic_related_manager(superclass) + + manager = RelatedManager( + model = rel_model, + instance = instance, + symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model), + join_table = backend.quote_name(self.field.m2m_db_table()), + source_col_name = backend.quote_name(self.field.m2m_column_name()), + target_col_name = backend.quote_name(self.field.m2m_reverse_name()), + content_type = ContentType.objects.get_for_model(self.field.model), + content_type_field_name = self.field.content_type_field_name, + object_id_field_name = self.field.object_id_field_name + ) + + return manager + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + manager = self.__get__(instance) + manager.clear() + for obj in value: + manager.add(obj) + +def create_generic_related_manager(superclass): + """ + Factory function for a manager that subclasses 'superclass' (which is a + Manager) and adds behavior for generic related objects. + """ + + class GenericRelatedObjectManager(superclass): + def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, + join_table=None, source_col_name=None, target_col_name=None, content_type=None, + content_type_field_name=None, object_id_field_name=None): + + super(GenericRelatedObjectManager, self).__init__() + self.core_filters = core_filters or {} + self.model = model + self.content_type = content_type + self.symmetrical = symmetrical + self.instance = instance + self.join_table = join_table + self.join_table = model._meta.db_table + self.source_col_name = source_col_name + self.target_col_name = target_col_name + self.content_type_field_name = content_type_field_name + self.object_id_field_name = object_id_field_name + self.pk_val = self.instance._get_pk_val() + + def get_query_set(self): + query = { + '%s__pk' % self.content_type_field_name : self.content_type.id, + '%s__exact' % self.object_id_field_name : self.pk_val, + } + return superclass.get_query_set(self).filter(**query) + + def add(self, *objs): + for obj in objs: + setattr(obj, self.content_type_field_name, self.content_type) + setattr(obj, self.object_id_field_name, self.pk_val) + obj.save() + add.alters_data = True + + def remove(self, *objs): + for obj in objs: + obj.delete() + remove.alters_data = True + + def clear(self): + for obj in self.all(): + obj.delete() + clear.alters_data = True + + def create(self, **kwargs): + kwargs[self.content_type_field_name] = self.content_type + kwargs[self.object_id_field_name] = self.pk_val + obj = self.model(**kwargs) + obj.save() + return obj + create.alters_data = True + + return GenericRelatedObjectManager + +class GenericRel(ManyToManyRel): + def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True): + self.to = to + self.num_in_admin = 0 + self.related_name = related_name + self.filter_interface = None + self.limit_choices_to = limit_choices_to or {} + self.edit_inline = False + self.raw_id_admin = False + self.symmetrical = symmetrical + self.multiple = True + assert not (self.raw_id_admin and self.filter_interface), \ + "Generic relations may not use both raw_id_admin and filter_interface" diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index ef5f6a6b3e..bb67399f3b 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -54,3 +54,6 @@ class BaseCache(object): Returns True if the key is in the cache and has not expired. """ return self.get(key) is not None + + __contains__ = has_key + diff --git a/django/core/management.py b/django/core/management.py index 47f44f8c8d..91de11f3b3 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -260,14 +260,14 @@ def _get_sql_for_pending_references(model, pending_references): def _get_many_to_many_sql_for_model(model): from django.db import backend, get_creation_module - from django.db.models import GenericRel + from django.contrib.contenttypes import generic data_types = get_creation_module().DATA_TYPES opts = model._meta final_output = [] for f in opts.many_to_many: - if not isinstance(f.rel, GenericRel): + if not isinstance(f.rel, generic.GenericRel): tablespace = f.db_tablespace or opts.db_tablespace if tablespace and backend.supports_tablespaces and backend.autoindexes_primary_keys: tablespace_sql = ' ' + backend.get_tablespace_sql(tablespace, inline=True) diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index ccd60023f9..6c3abb6b59 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -8,7 +8,6 @@ from django.db.models.manager import Manager from django.db.models.base import Model, AdminOptions from django.db.models.fields import * from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED -from django.db.models.fields.generic import GenericRelation, GenericRel, GenericForeignKey from django.db.models import signals from django.utils.functional import curry from django.utils.text import capfirst diff --git a/django/db/models/base.py b/django/db/models/base.py index eb95aae4f2..a567f0ed37 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -42,11 +42,11 @@ class ModelBase(type): new_class._meta.parents.append(base) new_class._meta.parents.extend(base._meta.parents) - model_module = sys.modules[new_class.__module__] if getattr(new_class._meta, 'app_label', None) is None: # Figure out the app_label by looking one level up. # For 'django.contrib.sites.models', this would be 'sites'. + model_module = sys.modules[new_class.__module__] new_class._meta.app_label = model_module.__name__.split('.')[-2] # Bail out early if we have already created this class. diff --git a/django/db/models/fields/generic.py b/django/db/models/fields/generic.py deleted file mode 100644 index f995ab2044..0000000000 --- a/django/db/models/fields/generic.py +++ /dev/null @@ -1,260 +0,0 @@ -""" -Classes allowing "generic" relations through ContentType and object-id fields. -""" - -from django import oldforms -from django.core.exceptions import ObjectDoesNotExist -from django.db import backend -from django.db.models import signals -from django.db.models.fields.related import RelatedField, Field, ManyToManyRel -from django.db.models.loading import get_model -from django.dispatch import dispatcher -from django.utils.functional import curry - -class GenericForeignKey(object): - """ - Provides a generic relation to any object through content-type/object-id - fields. - """ - - def __init__(self, ct_field="content_type", fk_field="object_id"): - self.ct_field = ct_field - self.fk_field = fk_field - - def contribute_to_class(self, cls, name): - # Make sure the fields exist (these raise FieldDoesNotExist, - # which is a fine error to raise here) - self.name = name - self.model = cls - self.cache_attr = "_%s_cache" % name - - # For some reason I don't totally understand, using weakrefs here doesn't work. - dispatcher.connect(self.instance_pre_init, signal=signals.pre_init, sender=cls, weak=False) - - # Connect myself as the descriptor for this field - setattr(cls, name, self) - - def instance_pre_init(self, signal, sender, args, kwargs): - # Handle initalizing an object with the generic FK instaed of - # content-type/object-id fields. - if self.name in kwargs: - value = kwargs.pop(self.name) - kwargs[self.ct_field] = self.get_content_type(value) - kwargs[self.fk_field] = value._get_pk_val() - - def get_content_type(self, obj): - # Convenience function using get_model avoids a circular import when using this model - ContentType = get_model("contenttypes", "contenttype") - return ContentType.objects.get_for_model(obj) - - def __get__(self, instance, instance_type=None): - if instance is None: - raise AttributeError, "%s must be accessed via instance" % self.name - - try: - return getattr(instance, self.cache_attr) - except AttributeError: - rel_obj = None - ct = getattr(instance, self.ct_field) - if ct: - try: - rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field)) - except ObjectDoesNotExist: - pass - setattr(instance, self.cache_attr, rel_obj) - return rel_obj - - def __set__(self, instance, value): - if instance is None: - raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name - - ct = None - fk = None - if value is not None: - ct = self.get_content_type(value) - fk = value._get_pk_val() - - setattr(instance, self.ct_field, ct) - setattr(instance, self.fk_field, fk) - setattr(instance, self.cache_attr, value) - -class GenericRelation(RelatedField, Field): - """Provides an accessor to generic related objects (i.e. comments)""" - - def __init__(self, to, **kwargs): - kwargs['verbose_name'] = kwargs.get('verbose_name', None) - kwargs['rel'] = GenericRel(to, - related_name=kwargs.pop('related_name', None), - limit_choices_to=kwargs.pop('limit_choices_to', None), - symmetrical=kwargs.pop('symmetrical', True)) - - # Override content-type/object-id field names on the related class - self.object_id_field_name = kwargs.pop("object_id_field", "object_id") - self.content_type_field_name = kwargs.pop("content_type_field", "content_type") - - kwargs['blank'] = True - kwargs['editable'] = False - kwargs['serialize'] = False - Field.__init__(self, **kwargs) - - def get_manipulator_field_objs(self): - choices = self.get_choices_default() - return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] - - def get_choices_default(self): - return Field.get_choices(self, include_blank=False) - - def flatten_data(self, follow, obj = None): - new_data = {} - if obj: - instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()] - new_data[self.name] = instance_ids - return new_data - - def m2m_db_table(self): - return self.rel.to._meta.db_table - - def m2m_column_name(self): - return self.object_id_field_name - - def m2m_reverse_name(self): - return self.object_id_field_name - - def contribute_to_class(self, cls, name): - super(GenericRelation, self).contribute_to_class(cls, name) - - # Save a reference to which model this class is on for future use - self.model = cls - - # Add the descriptor for the m2m relation - setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self)) - - def contribute_to_related_class(self, cls, related): - pass - - def set_attributes_from_rel(self): - pass - - def get_internal_type(self): - return "ManyToManyField" - -class ReverseGenericRelatedObjectsDescriptor(object): - """ - This class provides the functionality that makes the related-object - managers available as attributes on a model class, for fields that have - multiple "remote" values and have a GenericRelation defined in their model - (rather than having another model pointed *at* them). In the example - "article.publications", the publications attribute is a - ReverseGenericRelatedObjectsDescriptor instance. - """ - def __init__(self, field): - self.field = field - - def __get__(self, instance, instance_type=None): - if instance is None: - raise AttributeError, "Manager must be accessed via instance" - - # This import is done here to avoid circular import importing this module - from django.contrib.contenttypes.models import ContentType - - # Dynamically create a class that subclasses the related model's - # default manager. - rel_model = self.field.rel.to - superclass = rel_model._default_manager.__class__ - RelatedManager = create_generic_related_manager(superclass) - - manager = RelatedManager( - model = rel_model, - instance = instance, - symmetrical = (self.field.rel.symmetrical and instance.__class__ == rel_model), - join_table = backend.quote_name(self.field.m2m_db_table()), - source_col_name = backend.quote_name(self.field.m2m_column_name()), - target_col_name = backend.quote_name(self.field.m2m_reverse_name()), - content_type = ContentType.objects.get_for_model(self.field.model), - content_type_field_name = self.field.content_type_field_name, - object_id_field_name = self.field.object_id_field_name - ) - - return manager - - def __set__(self, instance, value): - if instance is None: - raise AttributeError, "Manager must be accessed via instance" - - manager = self.__get__(instance) - manager.clear() - for obj in value: - manager.add(obj) - -def create_generic_related_manager(superclass): - """ - Factory function for a manager that subclasses 'superclass' (which is a - Manager) and adds behavior for generic related objects. - """ - - class GenericRelatedObjectManager(superclass): - def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, - join_table=None, source_col_name=None, target_col_name=None, content_type=None, - content_type_field_name=None, object_id_field_name=None): - - super(GenericRelatedObjectManager, self).__init__() - self.core_filters = core_filters or {} - self.model = model - self.content_type = content_type - self.symmetrical = symmetrical - self.instance = instance - self.join_table = join_table - self.join_table = model._meta.db_table - self.source_col_name = source_col_name - self.target_col_name = target_col_name - self.content_type_field_name = content_type_field_name - self.object_id_field_name = object_id_field_name - self.pk_val = self.instance._get_pk_val() - - def get_query_set(self): - query = { - '%s__pk' % self.content_type_field_name : self.content_type.id, - '%s__exact' % self.object_id_field_name : self.pk_val, - } - return superclass.get_query_set(self).filter(**query) - - def add(self, *objs): - for obj in objs: - setattr(obj, self.content_type_field_name, self.content_type) - setattr(obj, self.object_id_field_name, self.pk_val) - obj.save() - add.alters_data = True - - def remove(self, *objs): - for obj in objs: - obj.delete() - remove.alters_data = True - - def clear(self): - for obj in self.all(): - obj.delete() - clear.alters_data = True - - def create(self, **kwargs): - kwargs[self.content_type_field_name] = self.content_type - kwargs[self.object_id_field_name] = self.pk_val - obj = self.model(**kwargs) - obj.save() - return obj - create.alters_data = True - - return GenericRelatedObjectManager - -class GenericRel(ManyToManyRel): - def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True): - self.to = to - self.num_in_admin = 0 - self.related_name = related_name - self.filter_interface = None - self.limit_choices_to = limit_choices_to or {} - self.edit_inline = False - self.raw_id_admin = False - self.symmetrical = symmetrical - self.multiple = True - assert not (self.raw_id_admin and self.filter_interface), \ - "Generic relations may not use both raw_id_admin and filter_interface" diff --git a/django/db/models/query.py b/django/db/models/query.py index d31ccf003e..e3b9c794f8 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,10 +1,9 @@ from django.db import backend, connection, 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.db.models import signals, loading from django.dispatch import dispatcher from django.utils.datastructures import SortedDict -from django.conf import settings +from django.contrib.contenttypes import generic import datetime import operator import re @@ -1091,7 +1090,7 @@ 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(): - if not isinstance(related.field, GenericRelation): + if not isinstance(related.field, generic.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()), @@ -1099,7 +1098,7 @@ def delete_objects(seen_objs): ','.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): + if isinstance(f, generic.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] diff --git a/django/template/__init__.py b/django/template/__init__.py index 4cb4f21156..9811a5649d 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -99,6 +99,10 @@ libraries = {} # global list of libraries to load by default for a new parser builtins = [] +# True if TEMPLATE_STRING_IF_INVALID contains a format string (%s). None means +# uninitialised. +invalid_var_format_string = None + class TemplateSyntaxError(Exception): def __str__(self): try: @@ -575,6 +579,11 @@ class FilterExpression(object): obj = None else: if settings.TEMPLATE_STRING_IF_INVALID: + global invalid_var_format_string + if invalid_var_format_string is None: + invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID + if invalid_var_format_string: + return settings.TEMPLATE_STRING_IF_INVALID % self.var return settings.TEMPLATE_STRING_IF_INVALID else: obj = settings.TEMPLATE_STRING_IF_INVALID diff --git a/django/test/testcases.py b/django/test/testcases.py index 80f55b20d3..153931f42a 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -1,7 +1,7 @@ import re, doctest, unittest from urlparse import urlparse from django.db import transaction -from django.core import management +from django.core import management, mail from django.db.models import get_apps from django.test.client import Client @@ -33,23 +33,27 @@ class DocTestRunner(doctest.DocTestRunner): transaction.rollback_unless_managed() class TestCase(unittest.TestCase): - def install_fixtures(self): - """If the Test Case class has a 'fixtures' member, clear the database and - install the named fixtures at the start of each test. + def _pre_setup(self): + """Perform any pre-test setup. This includes: + * If the Test Case class has a 'fixtures' member, clearing the + database and installing the named fixtures at the start of each test. + * Clearing the mail test outbox. + """ management.flush(verbosity=0, interactive=False) if hasattr(self, 'fixtures'): management.load_data(self.fixtures, verbosity=0) - + mail.outbox = [] + def run(self, result=None): - """Wrapper around default run method so that user-defined Test Cases - automatically call install_fixtures without having to include a call to - super(). + """Wrapper around default run method to perform common Django test set up. + This means that user-defined Test Cases aren't required to include a call + to super().setUp(). """ self.client = Client() - self.install_fixtures() + self._pre_setup() super(TestCase, self).run(result) def assertRedirects(self, response, expected_path): diff --git a/django/test/utils.py b/django/test/utils.py index 70b7f3cdbe..da339bb808 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,7 +1,7 @@ import sys, time from django.conf import settings from django.db import connection, backend, get_creation_module -from django.core import management +from django.core import management, mail from django.dispatch import dispatcher from django.test import signals from django.template import Template @@ -18,24 +18,54 @@ def instrumented_test_render(self, context): dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context) return self.nodelist.render(context) +class TestSMTPConnection(object): + """A substitute SMTP connection for use during test sessions. + The test connection stores email messages in a dummy outbox, + rather than sending them out on the wire. + + """ + def __init__(*args, **kwargs): + pass + def open(self): + "Mock the SMTPConnection open() interface" + pass + def close(self): + "Mock the SMTPConnection close() interface" + pass + def send_messages(self, messages): + "Redirect messages to the dummy outbox" + mail.outbox.extend(messages) + def setup_test_environment(): """Perform any global pre-test setup. This involves: - Installing the instrumented test renderer + - Diverting the email sending functions to a test buffer """ Template.original_render = Template.render Template.render = instrumented_test_render + mail.original_SMTPConnection = mail.SMTPConnection + mail.SMTPConnection = TestSMTPConnection + + mail.outbox = [] + def teardown_test_environment(): """Perform any global post-test teardown. This involves: - Restoring the original test renderer + - Restoring the email sending functions """ Template.render = Template.original_render del Template.original_render + mail.SMTPConnection = mail.original_SMTPConnection + del mail.original_SMTPConnection + + del mail.outbox + def _set_autocommit(connection): "Make sure a connection is in autocommit mode." if hasattr(connection.connection, "autocommit"): diff --git a/docs/contributing.txt b/docs/contributing.txt index 1d2b635b76..d05c166b37 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -396,10 +396,11 @@ To run the tests, ``cd`` to the ``tests/`` directory and type:: ./runtests.py --settings=path.to.django.settings Yes, the unit tests need a settings module, but only for database connection -info -- the ``DATABASE_ENGINE``, ``DATABASE_USER`` and ``DATABASE_PASSWORD``. -You will also need a ``ROOT_URLCONF`` setting (its value is ignored; it just -needs to be present) and a ``SITE_ID`` setting (any integer value will do) in -order for all the tests to pass. +info -- the ``DATABASE_NAME`` (required, but will be ignored), +``DATABASE_ENGINE``, ``DATABASE_USER`` and ``DATABASE_PASSWORD`` settings. You +will also need a ``ROOT_URLCONF`` setting (its value is ignored; it just needs +to be present) and a ``SITE_ID`` setting (any integer value will do) in order +for all the tests to pass. The unit tests will not touch your existing databases; they create a new database, called ``django_test_db``, which is deleted when the tests are diff --git a/docs/i18n.txt b/docs/i18n.txt index 56e6f7e02c..1d7a0063b2 100644 --- a/docs/i18n.txt +++ b/docs/i18n.txt @@ -310,7 +310,7 @@ To create or update a message file, run this command:: ...where ``de`` is the language code for the message file you want to create. The language code, in this case, is in locale format. For example, it's -``pt_BR`` for Brazilian and ``de_AT`` for Austrian German. +``pt_BR`` for Brazilian Portugese and ``de_AT`` for Austrian German. The script should be run from one of three places: @@ -463,8 +463,8 @@ following this algorithm: Notes: * In each of these places, the language preference is expected to be in the - standard language format, as a string. For example, Brazilian is - ``pt-br``. + standard language format, as a string. For example, Brazilian Portugese + is ``pt-br``. * If a base language is available but the sublanguage specified is not, Django uses the base language. For example, if a user specifies ``de-at`` (Austrian German) but Django only has ``de`` available, Django uses diff --git a/docs/model-api.txt b/docs/model-api.txt index a14c469661..961269aebd 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -459,7 +459,7 @@ string, not ``NULL``. ``blank`` ~~~~~~~~~ -If ``True``, the field is allowed to be blank. +If ``True``, the field is allowed to be blank. Default is ``False``. Note that this is different than ``null``. ``null`` is purely database-related, whereas ``blank`` is validation-related. If a field has diff --git a/docs/serialization.txt b/docs/serialization.txt index 8af4da26a8..3216cb061e 100644 --- a/docs/serialization.txt +++ b/docs/serialization.txt @@ -109,7 +109,7 @@ serializer, you must pass ``ensure_ascii=False`` as a parameter to the For example:: - json_serializer = serializers.get_serializer("json") + json_serializer = serializers.get_serializer("json")() json_serializer.serialize(queryset, ensure_ascii=False, stream=response) Writing custom serializers diff --git a/docs/sitemaps.txt b/docs/sitemaps.txt index dafc009859..550f448de1 100644 --- a/docs/sitemaps.txt +++ b/docs/sitemaps.txt @@ -2,8 +2,6 @@ The sitemap framework ===================== -**New in Django development version**. - Django comes with a high-level sitemap-generating framework that makes creating sitemap_ XML files easy. diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 1eeede1fe8..853707f58c 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -212,21 +212,24 @@ template tags. If an invalid variable is provided to one of these template tags, the variable will be interpreted as ``None``. Filters are always applied to invalid variables within these template tags. +If ``TEMPLATE_STRING_IF_INVALID`` contains a ``'%s'``, the format marker will +be replaced with the name of the invalid variable. + .. admonition:: For debug purposes only! - While ``TEMPLATE_STRING_IF_INVALID`` can be a useful debugging tool, - it is a bad idea to turn it on as a 'development default'. + While ``TEMPLATE_STRING_IF_INVALID`` can be a useful debugging tool, + it is a bad idea to turn it on as a 'development default'. - Many templates, including those in the Admin site, rely upon the - silence of the template system when a non-existent variable is + Many templates, including those in the Admin site, rely upon the + silence of the template system when a non-existent variable is encountered. If you assign a value other than ``''`` to - ``TEMPLATE_STRING_IF_INVALID``, you will experience rendering + ``TEMPLATE_STRING_IF_INVALID``, you will experience rendering problems with these templates and sites. - Generally, ``TEMPLATE_STRING_IF_INVALID`` should only be enabled - in order to debug a specific template problem, then cleared + Generally, ``TEMPLATE_STRING_IF_INVALID`` should only be enabled + in order to debug a specific template problem, then cleared once debugging is complete. - + Playing with Context objects ---------------------------- @@ -866,7 +869,7 @@ current context, available in the ``render`` method:: try: actual_date = resolve_variable(self.date_to_be_formatted, context) return actual_date.strftime(self.format_string) - except VariableDoesNotExist: + except template.VariableDoesNotExist: return '' ``resolve_variable`` will try to resolve ``blog_entry.date_updated`` and then diff --git a/docs/testing.txt b/docs/testing.txt index b3b33e9678..ba13dab67e 100644 --- a/docs/testing.txt +++ b/docs/testing.txt @@ -177,6 +177,7 @@ tools that can be used to establish tests and test conditions. * `Test Client`_ * `TestCase`_ +* `Email services`_ Test Client ----------- @@ -257,7 +258,7 @@ can be invoked on the ``Client`` instance. need to manually close the file after it has been provided to the POST. ``login(**credentials)`` - ** New in Django development version ** + **New in Django development version** On a production site, it is likely that some views will be protected from anonymous access through the use of the @login_required decorator, or some @@ -289,9 +290,9 @@ can be invoked on the ``Client`` instance. Testing Responses ~~~~~~~~~~~~~~~~~ -The ``get()``, ``post()`` and ``login()`` methods all return a Response -object. This Response object has the following properties that can be used -for testing purposes: +The ``get()`` and ``post()`` methods both return a Response object. This +Response object has the following properties that can be used for testing +purposes: =============== ========================================================== Property Description @@ -396,7 +397,7 @@ extra facilities. Default Test Client ~~~~~~~~~~~~~~~~~~~ -** New in Django development version ** +**New in Django development version** Every test case in a ``django.test.TestCase`` instance has access to an instance of a Django `Test Client`_. This Client can be accessed as @@ -453,9 +454,18 @@ This flush/load procedure is repeated for each test in the test case, so you can be certain that the outcome of a test will not be affected by another test, or the order of test execution. +Emptying the test outbox +~~~~~~~~~~~~~~~~~~~~~~~~ +**New in Django development version** + +At the start of each test case, in addition to installing fixtures, +Django clears the contents of the test email outbox. + +For more detail on email services during tests, see `Email services`_. + Assertions ~~~~~~~~~~ -** New in Django development version ** +**New in Django development version** Normal Python unit tests have a wide range of assertions, such as ``assertTrue`` and ``assertEquals`` that can be used to validate behavior. @@ -468,30 +478,73 @@ that can be useful in testing the behavior of web sites. times in the content of the response. ``assertFormError(response, form, field, errors)`` - Assert that a field on a form raised the provided list of errors when - rendered on the form. - - ``form`` is the name the form object was given in the template context. - - ``field`` is the name of the field on the form to check. If ``field`` + Assert that a field on a form raised the provided list of errors when + rendered on the form. + + ``form`` is the name the form object was given in the template context. + + ``field`` is the name of the field on the form to check. If ``field`` has a value of ``None``, non-field errors will be checked. - - ``errors`` is an error string, or a list of error strings, that are - expected as a result of form validation. - + + ``errors`` is an error string, or a list of error strings, that are + expected as a result of form validation. + ``assertTemplateNotUsed(response, template_name)`` - Assert that the template with the given name was *not* used in rendering + Assert that the template with the given name was *not* used in rendering the response. - + ``assertRedirects(response, expected_path)`` Assert that the response received redirects the browser to the provided - path, and that the expected_path can be retrieved. + path, and that the expected_path can be retrieved. ``assertTemplateUsed(response, template_name)`` Assert that the template with the given name was used in rendering the response. - - + +Email services +-------------- +**New in Django development version** + +If your view makes use of the `Django email services`_, you don't really +want email to be sent every time you run a test using that view. + +When the Django test framework is initialized, it transparently replaces the +normal `SMTPConnection`_ class with a dummy implementation that redirects all +email to a dummy outbox. This outbox, stored as ``django.core.mail.outbox``, +is a simple list of all `EmailMessage`_ instances that have been sent. +For example, during test conditions, it would be possible to run the following +code:: + + from django.core import mail + + # Send message + mail.send_mail('Subject here', 'Here is the message.', 'from@example.com', + ['to@example.com'], fail_silently=False) + + # One message has been sent + self.assertEqual(len(mail.outbox), 1) + # Subject of first message is correct + self.assertEqual(mail.outbox[0].subject, 'Subject here') + +The ``mail.outbox`` object does not exist under normal execution conditions. +The outbox is created during test setup, along with the dummy `SMTPConnection`_. +When the test framework is torn down, the standard `SMTPConnection`_ class +is restored, and the test outbox is destroyed. + +As noted `previously`_, the test outbox is emptied at the start of every +test in a Django TestCase. To empty the outbox manually, assign the empty list +to mail.outbox:: + + from django.core import mail + + # Empty the test outbox + mail.outbox = [] + +.. _`Django email services`: ../email/ +.. _`SMTPConnection`: ../email/#the-emailmessage-and-smtpconnection-classes +.. _`EmailMessage`: ../email/#the-emailmessage-and-smtpconnection-classes +.. _`previously`: #emptying-the-test-outbox + Running tests ============= @@ -516,6 +569,10 @@ database settings will the same as they would be for the project normally. If you wish to use a name other than the default for the test database, you can use the ``TEST_DATABASE_NAME`` setting to provide a name. +The test database is created by the user in the ``DATABASE_USER`` setting. +This user needs to have sufficient privileges to create a new database on the +system. + Once the test database has been established, Django will run your tests. If everything goes well, at the end you'll see:: @@ -606,11 +663,12 @@ a number of utility methods in the ``django.test.utils`` module. ``setup_test_environment()`` Performs any global pre-test setup, such as the installing the - instrumentation of the template rendering system. + instrumentation of the template rendering system and setting up + the dummy SMTPConnection. ``teardown_test_environment()`` Performs any global post-test teardown, such as removing the instrumentation - of the template rendering system. + of the template rendering system and restoring normal email services. ``create_test_db(verbosity=1, autoclobber=False)`` Creates a new test database, and run ``syncdb`` against it. diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py index 2b2f64165f..195f67db8f 100644 --- a/tests/modeltests/generic_relations/models.py +++ b/tests/modeltests/generic_relations/models.py @@ -11,6 +11,7 @@ from complete). from django.db import models from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic class TaggedItem(models.Model): """A tag on an item.""" @@ -18,7 +19,7 @@ class TaggedItem(models.Model): content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() - content_object = models.GenericForeignKey() + content_object = generic.GenericForeignKey() class Meta: ordering = ["tag"] @@ -30,7 +31,7 @@ class Animal(models.Model): common_name = models.CharField(maxlength=150) latin_name = models.CharField(maxlength=150) - tags = models.GenericRelation(TaggedItem) + tags = generic.GenericRelation(TaggedItem) def __str__(self): return self.common_name @@ -39,7 +40,7 @@ class Vegetable(models.Model): name = models.CharField(maxlength=150) is_yucky = models.BooleanField(default=True) - tags = models.GenericRelation(TaggedItem) + tags = generic.GenericRelation(TaggedItem) def __str__(self): return self.name diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index cd8dbe37d2..34242ee0d8 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -20,6 +20,7 @@ rather than the HTML rendered to the end-user. """ from django.test import Client, TestCase +from django.core import mail class ClientTest(TestCase): fixtures = ['testdata.json'] @@ -232,3 +233,36 @@ class ClientTest(TestCase): self.fail('Should raise an error') except KeyError: pass + + def test_mail_sending(self): + "Test that mail is redirected to a dummy outbox during test setup" + + response = self.client.get('/test_client/mail_sending_view/') + self.assertEqual(response.status_code, 200) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, 'Test message') + self.assertEqual(mail.outbox[0].body, 'This is a test email') + self.assertEqual(mail.outbox[0].from_email, 'from@example.com') + self.assertEqual(mail.outbox[0].to[0], 'first@example.com') + self.assertEqual(mail.outbox[0].to[1], 'second@example.com') + + def test_mass_mail_sending(self): + "Test that mass mail is redirected to a dummy outbox during test setup" + + response = self.client.get('/test_client/mass_mail_sending_view/') + self.assertEqual(response.status_code, 200) + + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[0].subject, 'First Test message') + self.assertEqual(mail.outbox[0].body, 'This is the first test email') + self.assertEqual(mail.outbox[0].from_email, 'from@example.com') + self.assertEqual(mail.outbox[0].to[0], 'first@example.com') + self.assertEqual(mail.outbox[0].to[1], 'second@example.com') + + self.assertEqual(mail.outbox[1].subject, 'Second Test message') + self.assertEqual(mail.outbox[1].body, 'This is the second test email') + self.assertEqual(mail.outbox[1].from_email, 'from@example.com') + self.assertEqual(mail.outbox[1].to[0], 'second@example.com') + self.assertEqual(mail.outbox[1].to[1], 'third@example.com') + \ No newline at end of file diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py index f63c486d01..52fc8fe692 100644 --- a/tests/modeltests/test_client/urls.py +++ b/tests/modeltests/test_client/urls.py @@ -11,5 +11,7 @@ urlpatterns = patterns('', (r'^form_view_with_template/$', views.form_view_with_template), (r'^login_protected_view/$', views.login_protected_view), (r'^session_view/$', views.session_view), - (r'^broken_view/$', views.broken_view) + (r'^broken_view/$', views.broken_view), + (r'^mail_sending_view/$', views.mail_sending_view), + (r'^mass_mail_sending_view/$', views.mass_mail_sending_view) ) diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py index 3b7a57f4d0..18d6a2dcd9 100644 --- a/tests/modeltests/test_client/views.py +++ b/tests/modeltests/test_client/views.py @@ -1,4 +1,5 @@ from xml.dom.minidom import parseString +from django.core.mail import EmailMessage, SMTPConnection from django.template import Context, Template from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.decorators import login_required @@ -124,3 +125,28 @@ def session_view(request): def broken_view(request): """A view which just raises an exception, simulating a broken view.""" raise KeyError("Oops! Looks like you wrote some bad code.") + +def mail_sending_view(request): + EmailMessage( + "Test message", + "This is a test email", + "from@example.com", + ['first@example.com', 'second@example.com']).send() + return HttpResponse("Mail sent") + +def mass_mail_sending_view(request): + m1 = EmailMessage( + 'First Test message', + 'This is the first test email', + 'from@example.com', + ['first@example.com', 'second@example.com']) + m2 = EmailMessage( + 'Second Test message', + 'This is the second test email', + 'from@example.com', + ['second@example.com', 'third@example.com']) + + c = SMTPConnection() + c.send_messages([m1,m2]) + + return HttpResponse("Mail sent") diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index cf58ab321a..9dc7161c03 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -46,6 +46,11 @@ class Cache(unittest.TestCase): self.assertEqual(cache.has_key("hello"), True) self.assertEqual(cache.has_key("goodbye"), False) + def test_in(self): + cache.set("hello", "goodbye") + self.assertEqual("hello" in cache, True) + self.assertEqual("goodbye" in cache, False) + def test_data_types(self): # test data types stuff = { diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py index d3415ac1b9..c287b6e0d6 100644 --- a/tests/regressiontests/serializers_regress/models.py +++ b/tests/regressiontests/serializers_regress/models.py @@ -6,6 +6,7 @@ This class sets up a model for each model field type """ from django.db import models +from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType # The following classes are for testing basic data @@ -80,7 +81,7 @@ class Tag(models.Model): content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() - content_object = models.GenericForeignKey() + content_object = generic.GenericForeignKey() class Meta: ordering = ["data"] @@ -88,7 +89,7 @@ class Tag(models.Model): class GenericData(models.Model): data = models.CharField(maxlength=30) - tags = models.GenericRelation(Tag) + tags = generic.GenericRelation(Tag) # The following test classes are all for validation # of related objects; in particular, forward, backward, diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 9be8f022f6..a5ed2dbf56 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -586,6 +586,8 @@ class Templates(unittest.TestCase): 'invalidstr03': ('{% for v in var %}({{ v }}){% endfor %}', {}, ''), 'invalidstr04': ('{% if var %}Yes{% else %}No{% endif %}', {}, 'No'), 'invalidstr04': ('{% if var|default:"Foo" %}Yes{% else %}No{% endif %}', {}, 'Yes'), + 'invalidstr05': ('{{ var }}', {}, ('', 'INVALID %s', 'var')), + 'invalidstr06': ('{{ var.prop }}', {'var': {}}, ('', 'INVALID %s', 'var.prop')), ### MULTILINE ############################################################# @@ -737,6 +739,7 @@ class Templates(unittest.TestCase): # Set TEMPLATE_STRING_IF_INVALID to a known string old_invalid = settings.TEMPLATE_STRING_IF_INVALID + expected_invalid_str = 'INVALID' for name, vals in tests: install() @@ -744,6 +747,10 @@ class Templates(unittest.TestCase): if isinstance(vals[2], tuple): normal_string_result = vals[2][0] invalid_string_result = vals[2][1] + if '%s' in invalid_string_result: + expected_invalid_str = 'INVALID %s' + invalid_string_result = invalid_string_result % vals[2][2] + template.invalid_var_format_string = True else: normal_string_result = vals[2] invalid_string_result = vals[2] @@ -754,7 +761,7 @@ class Templates(unittest.TestCase): activate('en-us') for invalid_str, result in [('', normal_string_result), - ('INVALID', invalid_string_result)]: + (expected_invalid_str, invalid_string_result)]: settings.TEMPLATE_STRING_IF_INVALID = invalid_str try: output = loader.get_template(name).render(template.Context(vals[1])) @@ -768,6 +775,10 @@ class Templates(unittest.TestCase): if 'LANGUAGE_CODE' in vals[1]: deactivate() + if template.invalid_var_format_string: + expected_invalid_str = 'INVALID' + template.invalid_var_format_string = False + loader.template_source_loaders = old_template_loaders deactivate() settings.TEMPLATE_DEBUG = old_td -- cgit v1.2.1