diff --git a/AUTHORS b/AUTHORS index 7e620f89a67ec..75ad94ec1ff8f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -89,6 +89,7 @@ answer newbie questions, and generally made Django that much better: David Avsajanishvili Mike Axiak Niran Babalola + Christopher Babiak Vitaly Babiy Morten Bagai Jeff Balogh diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index 4f04a474dd534..9c5fbf7d8cef8 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -303,6 +303,11 @@ def __init__(self, field, request, params, model, model_admin, field_path): else: # field is a models.DateField today = now.date() tomorrow = today + datetime.timedelta(days=1) + if today.month == 12: + next_month = today.replace(year=today.year + 1, month=1, day=1) + else: + next_month = today.replace(month=today.month + 1, day=1) + next_year = today.replace(year=today.year + 1, month=1, day=1) self.lookup_kwarg_since = '%s__gte' % field_path self.lookup_kwarg_until = '%s__lt' % field_path @@ -318,11 +323,11 @@ def __init__(self, field, request, params, model, model_admin, field_path): }), (_('This month'), { self.lookup_kwarg_since: str(today.replace(day=1)), - self.lookup_kwarg_until: str(tomorrow), + self.lookup_kwarg_until: str(next_month), }), (_('This year'), { self.lookup_kwarg_since: str(today.replace(month=1, day=1)), - self.lookup_kwarg_until: str(tomorrow), + self.lookup_kwarg_until: str(next_year), }), ) super(DateFieldListFilter, self).__init__( diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 9db672c8a33f7..f3d66eaf31524 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -319,6 +319,23 @@ def __init__(self, model=None, instance=None, symmetrical=None, '%s__exact' % object_id_field_name: instance._get_pk_val(), } + def __call__(self, **kwargs): + # We use **kwargs rather than a kwarg argument to enforce the + # `manager='manager_name'` syntax. + manager = getattr(self.model, kwargs.pop('manager')) + manager_class = create_generic_related_manager(manager.__class__) + return manager_class( + model = self.model, + instance = self.instance, + symmetrical = self.symmetrical, + source_col_name = self.source_col_name, + target_col_name = self.target_col_name, + content_type = self.content_type, + content_type_field_name = self.content_type_field_name, + object_id_field_name = self.object_id_field_name, + prefetch_cache_name = self.prefetch_cache_name, + ) + def get_queryset(self): try: return self.instance._prefetched_objects_cache[self.prefetch_cache_name] diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 0e59c6d65efc2..8844bc8f6141d 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -11,6 +11,7 @@ from django.conf import settings from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.db import connections, transaction, router, DatabaseError +from django.db.backends.utils import typecast_timestamp from django.utils import timezone, six from django.utils.encoding import force_bytes @@ -65,8 +66,13 @@ def get(self, key, default=None, version=None): if row is None: return default now = timezone.now() - - if row[2] < now: + expires = row[2] + if connections[db].features.needs_datetime_string_cast and not isinstance(expires, datetime): + # Note: typecasting is needed by some 3rd party database backends. + # All core backends work without typecasting, so be careful about + # changes here - test suite will NOT pick regressions here. + expires = typecast_timestamp(str(expires)) + if expires < now: db = router.db_for_write(self.cache_model_class) cursor = connections[db].cursor() cursor.execute("DELETE FROM %s " @@ -112,12 +118,21 @@ def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT): if six.PY3: b64encoded = b64encoded.decode('latin1') try: + # Note: typecasting for datetimes is needed by some 3rd party + # database backends. All core backends work without typecasting, + # so be careful about changes here - test suite will NOT pick + # regressions. with transaction.atomic(using=db): cursor.execute("SELECT cache_key, expires FROM %s " "WHERE cache_key = %%s" % table, [key]) result = cursor.fetchone() + if result: + current_expires = result[1] + if (connections[db].features.needs_datetime_string_cast and not + isinstance(current_expires, datetime)): + current_expires = typecast_timestamp(str(current_expires)) exp = connections[db].ops.value_to_db_datetime(exp) - if result and (mode == 'set' or (mode == 'add' and result[1] < now)): + if result and (mode == 'set' or (mode == 'add' and current_expires < now)): cursor.execute("UPDATE %s SET value = %%s, expires = %%s " "WHERE cache_key = %%s" % table, [b64encoded, exp, key]) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index ae9fdbba54e6a..74046a0d9b96e 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1,7 +1,7 @@ import datetime import time -from django.db.utils import DatabaseError +from django.db.utils import DatabaseError, ProgrammingError try: from django.utils.six.moves import _thread as thread @@ -664,6 +664,9 @@ class BaseDatabaseFeatures(object): # Does the backend require a connection reset after each material schema change? connection_persists_old_columns = False + # What kind of error does the backend throw when accessing closed cursor? + closed_cursor_error_class = ProgrammingError + def __init__(self, connection): self.connection = connection @@ -1167,7 +1170,7 @@ def convert_values(self, value, field): Coerce the value returned by the database backend into a consistent type that is compatible with the field type. """ - if value is None: + if value is None or field is None: return value internal_type = field.get_internal_type() if internal_type == 'FloatField': diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 71ff2811e287c..b22c653bd74b7 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -15,6 +15,7 @@ from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor +from django.db.utils import InterfaceError from django.utils.encoding import force_str from django.utils.functional import cached_property from django.utils.safestring import SafeText, SafeBytes @@ -60,6 +61,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): can_rollback_ddl = True supports_combined_alters = True nulls_order_largest = True + closed_cursor_error_class = InterfaceError class DatabaseWrapper(BaseDatabaseWrapper): diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py index 57d9a67abf4d5..046af9af55685 100644 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ b/django/db/backends/postgresql_psycopg2/introspection.py @@ -137,6 +137,7 @@ def get_constraints(self, cursor, table_name): WHERE kc.table_schema = %s AND kc.table_name = %s + ORDER BY kc.ordinal_position ASC """, ["public", table_name]) for constraint, column, kind, used_cols in cursor.fetchall(): # If we're the first column, make the record diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py index 0e2aa4503eae3..ccda77f6587b8 100644 --- a/django/db/backends/utils.py +++ b/django/db/backends/utils.py @@ -36,6 +36,14 @@ def __getattr__(self, attr): def __iter__(self): return iter(self.cursor) + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + # Ticket #17671 - Close instead of passing thru to avoid backend + # specific behavior. + self.close() + class CursorDebugWrapper(CursorWrapper): diff --git a/django/db/migrations/operations/special.py b/django/db/migrations/operations/special.py index 89b13c46a04e0..83cdea308ebd2 100644 --- a/django/db/migrations/operations/special.py +++ b/django/db/migrations/operations/special.py @@ -1,6 +1,7 @@ import re import textwrap from .base import Operation +from django.utils import six class SeparateDatabaseAndState(Operation): @@ -106,13 +107,25 @@ class RunPython(Operation): reduces_to_sql = False reversible = False - def __init__(self, code): - # Trim any leading whitespace that is at the start of all code lines - # so users can nicely indent code in migration files - code = textwrap.dedent(code) - # Run the code through a parser first to make sure it's at least - # syntactically correct - self.code = compile(code, "", "exec") + def __init__(self, code, reverse_code=None): + # Forwards code + if isinstance(code, six.string_types): + # Trim any leading whitespace that is at the start of all code lines + # so users can nicely indent code in migration files + code = textwrap.dedent(code) + # Run the code through a parser first to make sure it's at least + # syntactically correct + self.code = compile(code, "", "exec") + else: + self.code = code + # Reverse code + if reverse_code is None: + self.reverse_code = None + elif isinstance(reverse_code, six.string_types): + reverse_code = textwrap.dedent(reverse_code) + self.reverse_code = compile(reverse_code, "", "exec") + else: + self.reverse_code = reverse_code def state_forwards(self, app_label, state): # RunPython objects have no state effect. To add some, combine this @@ -124,14 +137,26 @@ def database_forwards(self, app_label, schema_editor, from_state, to_state): # object, representing the versioned models as an AppCache. # We could try to override the global cache, but then people will still # use direct imports, so we go with a documentation approach instead. - context = { - "models": from_state.render(), - "schema_editor": schema_editor, - } - eval(self.code, context) + if six.callable(self.code): + self.code(models=from_state.render(), schema_editor=schema_editor) + else: + context = { + "models": from_state.render(), + "schema_editor": schema_editor, + } + eval(self.code, context) def database_backwards(self, app_label, schema_editor, from_state, to_state): - raise NotImplementedError("You cannot reverse this operation") + if self.reverse_code is None: + raise NotImplementedError("You cannot reverse this operation") + elif six.callable(self.reverse_code): + self.reverse_code(models=from_state.render(), schema_editor=schema_editor) + else: + context = { + "models": from_state.render(), + "schema_editor": schema_editor, + } + eval(self.reverse_code, context) def describe(self): return "Raw Python operation" diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index e019855826b53..254e6854dfcef 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1312,6 +1312,8 @@ class IPAddressField(Field): description = _("IPv4 address") def __init__(self, *args, **kwargs): + warnings.warn("IPAddressField has been deprecated. Use GenericIPAddressField instead.", + PendingDeprecationWarning) kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index fd9e8fa4d88f1..23959f6b18280 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -365,6 +365,92 @@ def __set__(self, instance, value): setattr(value, self.field.related.get_cache_name(), instance) +def create_foreign_related_manager(superclass, rel_field, rel_model): + class RelatedManager(superclass): + def __init__(self, instance): + super(RelatedManager, self).__init__() + self.instance = instance + self.core_filters = {'%s__exact' % rel_field.name: instance} + self.model = rel_model + + def __call__(self, **kwargs): + # We use **kwargs rather than a kwarg argument to enforce the + # `manager='manager_name'` syntax. + manager = getattr(self.model, kwargs.pop('manager')) + manager_class = create_foreign_related_manager(manager.__class__, rel_field, rel_model) + return manager_class(self.instance) + + def get_queryset(self): + try: + return self.instance._prefetched_objects_cache[rel_field.related_query_name()] + except (AttributeError, KeyError): + db = self._db or router.db_for_read(self.model, instance=self.instance) + qs = super(RelatedManager, self).get_queryset().using(db).filter(**self.core_filters) + empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls + for field in rel_field.foreign_related_fields: + val = getattr(self.instance, field.attname) + if val is None or (val == '' and empty_strings_as_null): + return qs.none() + qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} + return qs + + def get_prefetch_queryset(self, instances): + rel_obj_attr = rel_field.get_local_related_value + instance_attr = rel_field.get_foreign_related_value + instances_dict = dict((instance_attr(inst), inst) for inst in instances) + db = self._db or router.db_for_read(self.model, instance=instances[0]) + query = {'%s__in' % rel_field.name: instances} + qs = super(RelatedManager, self).get_queryset().using(db).filter(**query) + # Since we just bypassed this class' get_queryset(), we must manage + # the reverse relation manually. + for rel_obj in qs: + instance = instances_dict[rel_obj_attr(rel_obj)] + setattr(rel_obj, rel_field.name, instance) + cache_name = rel_field.related_query_name() + return qs, rel_obj_attr, instance_attr, False, cache_name + + def add(self, *objs): + for obj in objs: + if not isinstance(obj, self.model): + raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) + setattr(obj, rel_field.name, self.instance) + obj.save() + add.alters_data = True + + def create(self, **kwargs): + kwargs[rel_field.name] = self.instance + db = router.db_for_write(self.model, instance=self.instance) + return super(RelatedManager, self.db_manager(db)).create(**kwargs) + create.alters_data = True + + def get_or_create(self, **kwargs): + # Update kwargs with the related object that this + # ForeignRelatedObjectsDescriptor knows about. + kwargs[rel_field.name] = self.instance + db = router.db_for_write(self.model, instance=self.instance) + return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs) + get_or_create.alters_data = True + + # remove() and clear() are only provided if the ForeignKey can have a value of null. + if rel_field.null: + def remove(self, *objs): + val = rel_field.get_foreign_related_value(self.instance) + for obj in objs: + # Is obj actually part of this descriptor set? + if rel_field.get_local_related_value(obj) == val: + setattr(obj, rel_field.name, None) + obj.save() + else: + raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance)) + remove.alters_data = True + + def clear(self): + self.update(**{rel_field.name: None}) + clear.alters_data = True + + return RelatedManager + + class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -392,86 +478,11 @@ def __set__(self, instance, value): def related_manager_cls(self): # Dynamically create a class that subclasses the related model's default # manager. - superclass = self.related.model._default_manager.__class__ - rel_field = self.related.field - rel_model = self.related.model - - class RelatedManager(superclass): - def __init__(self, instance): - super(RelatedManager, self).__init__() - self.instance = instance - self.core_filters = {'%s__exact' % rel_field.name: instance} - self.model = rel_model - - def get_queryset(self): - try: - return self.instance._prefetched_objects_cache[rel_field.related_query_name()] - except (AttributeError, KeyError): - db = self._db or router.db_for_read(self.model, instance=self.instance) - qs = super(RelatedManager, self).get_queryset().using(db).filter(**self.core_filters) - empty_strings_as_null = connections[db].features.interprets_empty_strings_as_nulls - for field in rel_field.foreign_related_fields: - val = getattr(self.instance, field.attname) - if val is None or (val == '' and empty_strings_as_null): - return qs.none() - qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} - return qs - - def get_prefetch_queryset(self, instances): - rel_obj_attr = rel_field.get_local_related_value - instance_attr = rel_field.get_foreign_related_value - instances_dict = dict((instance_attr(inst), inst) for inst in instances) - db = self._db or router.db_for_read(self.model, instance=instances[0]) - query = {'%s__in' % rel_field.name: instances} - qs = super(RelatedManager, self).get_queryset().using(db).filter(**query) - # Since we just bypassed this class' get_queryset(), we must manage - # the reverse relation manually. - for rel_obj in qs: - instance = instances_dict[rel_obj_attr(rel_obj)] - setattr(rel_obj, rel_field.name, instance) - cache_name = rel_field.related_query_name() - return qs, rel_obj_attr, instance_attr, False, cache_name - - def add(self, *objs): - for obj in objs: - if not isinstance(obj, self.model): - raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj)) - setattr(obj, rel_field.name, self.instance) - obj.save() - add.alters_data = True - - def create(self, **kwargs): - kwargs[rel_field.name] = self.instance - db = router.db_for_write(self.model, instance=self.instance) - return super(RelatedManager, self.db_manager(db)).create(**kwargs) - create.alters_data = True - - def get_or_create(self, **kwargs): - # Update kwargs with the related object that this - # ForeignRelatedObjectsDescriptor knows about. - kwargs[rel_field.name] = self.instance - db = router.db_for_write(self.model, instance=self.instance) - return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs) - get_or_create.alters_data = True - - # remove() and clear() are only provided if the ForeignKey can have a value of null. - if rel_field.null: - def remove(self, *objs): - val = rel_field.get_foreign_related_value(self.instance) - for obj in objs: - # Is obj actually part of this descriptor set? - if rel_field.get_local_related_value(obj) == val: - setattr(obj, rel_field.name, None) - obj.save() - else: - raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance)) - remove.alters_data = True - - def clear(self): - self.update(**{rel_field.name: None}) - clear.alters_data = True - - return RelatedManager + return create_foreign_related_manager( + self.related.model._default_manager.__class__, + self.related.field, + self.related.model, + ) def create_many_related_manager(superclass, rel): @@ -513,6 +524,23 @@ def __init__(self, model=None, query_field_name=None, instance=None, symmetrical "a many-to-many relationship can be used." % instance.__class__.__name__) + def __call__(self, **kwargs): + # We use **kwargs rather than a kwarg argument to enforce the + # `manager='manager_name'` syntax. + manager = getattr(self.model, kwargs.pop('manager')) + manager_class = create_many_related_manager(manager.__class__, rel) + return manager_class( + model=self.model, + query_field_name=self.query_field_name, + instance=self.instance, + symmetrical=self.symmetrical, + source_field_name=self.source_field_name, + target_field_name=self.target_field_name, + reverse=self.reverse, + through=self.through, + prefetch_cache_name=self.prefetch_cache_name, + ) + def get_queryset(self): try: return self.instance._prefetched_objects_cache[self.prefetch_cache_name] diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 4d571b78fa327..4a08b1fc14feb 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -686,6 +686,10 @@ def results_iter(self): has_aggregate_select = bool(self.query.aggregate_select) for rows in self.execute_sql(MULTI): for row in rows: + if has_aggregate_select: + loaded_fields = self.query.get_loaded_field_names().get(self.query.model, set()) or self.query.select + aggregate_start = len(self.query.extra_select) + len(loaded_fields) + aggregate_end = aggregate_start + len(self.query.aggregate_select) if resolve_columns: if fields is None: # We only set this up here because @@ -712,12 +716,14 @@ def results_iter(self): db_table = self.query.get_meta().db_table fields = [f for f in fields if db_table in only_load and f.column in only_load[db_table]] + if has_aggregate_select: + # pad None in to fields for aggregates + fields = fields[:aggregate_start] + [ + None for x in range(0, aggregate_end - aggregate_start) + ] + fields[aggregate_start:] row = self.resolve_columns(row, fields) if has_aggregate_select: - loaded_fields = self.query.get_loaded_field_names().get(self.query.model, set()) or self.query.select - aggregate_start = len(self.query.extra_select) + len(loaded_fields) - aggregate_end = aggregate_start + len(self.query.aggregate_select) row = tuple(row[:aggregate_start]) + tuple( self.query.resolve_aggregate(value, aggregate, self.connection) for (alias, aggregate), value diff --git a/django/forms/fields.py b/django/forms/fields.py index 7e4e5df833559..4e0ee29938b95 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -9,6 +9,7 @@ import os import re import sys +import warnings from decimal import Decimal, DecimalException from io import BytesIO @@ -1144,6 +1145,11 @@ def compress(self, data_list): class IPAddressField(CharField): default_validators = [validators.validate_ipv4_address] + def __init__(self, *args, **kwargs): + warnings.warn("IPAddressField has been deprecated. Use GenericIPAddressField instead.", + PendingDeprecationWarning) + super(IPAddressField, self).__init__(*args, **kwargs) + def to_python(self, value): if value in self.empty_values: return '' diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 372c0f440b1c3..f8320117f74e3 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -665,10 +665,6 @@ def __init__(self, name, value, attrs, choices): self.attrs = attrs self.choices = choices - def __iter__(self): - for i, choice in enumerate(self.choices): - yield self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i) - def __getitem__(self, idx): choice = self.choices[idx] # Let the IndexError propogate return self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, idx) @@ -685,8 +681,23 @@ def render(self): id_ = self.attrs.get('id', None) start_tag = format_html('
    ', id_) if id_ else '
      ' output = [start_tag] - for widget in self: - output.append(format_html('
    • {0}
    • ', force_text(widget))) + for i, choice in enumerate(self.choices): + choice_value, choice_label = choice + if isinstance(choice_label, (tuple,list)): + attrs_plus = self.attrs.copy() + if id_: + attrs_plus['id'] += '_{0}'.format(i) + sub_ul_renderer = ChoiceFieldRenderer(name=self.name, + value=self.value, + attrs=attrs_plus, + choices=choice_label) + sub_ul_renderer.choice_input_class = self.choice_input_class + output.append(format_html('
    • {0}{1}
    • ', choice_value, + sub_ul_renderer.render())) + else: + w = self.choice_input_class(self.name, self.value, + self.attrs.copy(), choice, i) + output.append(format_html('
    • {0}
    • ', force_text(w))) output.append('
    ') return mark_safe('\n'.join(output)) diff --git a/django/test/testcases.py b/django/test/testcases.py index a657530f94e9b..3f0046314e2e5 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -958,7 +958,11 @@ def get_response(self, request): def serve(self, request): os_rel_path = self.file_path(request.path) - final_rel_path = posixpath.normpath(unquote(os_rel_path)).lstrip('/') + os_rel_path = posixpath.normpath(unquote(os_rel_path)) + # Emulate behavior of django.contrib.staticfiles.views.serve() when it + # invokes staticfiles' finders functionality. + # TODO: Modify if/when that internal API is refactored + final_rel_path = os_rel_path.replace('\\', '/').lstrip('/') return serve(request, final_rel_path, document_root=self.get_base_dir()) def __call__(self, environ, start_response): diff --git a/django/utils/html.py b/django/utils/html.py index ff3bc60021891..75eff0083d2d4 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -15,8 +15,8 @@ # Configuration for urlize() function. -TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)'] -WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>')] +TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', '\''] +WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ('\'', '\'')] # List of possible strings used for bullets in bulleted lists. DOTS = ['·', '*', '\u2022', '•', '•', '•'] diff --git a/django/utils/text.py b/django/utils/text.py index cfa54c30ff9a5..66282691a89e1 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -439,12 +439,12 @@ def _replace_entity(match): c = int(text[1:], 16) else: c = int(text) - return unichr(c) + return six.unichr(c) except ValueError: return match.group(0) else: try: - return unichr(html_entities.name2codepoint[text]) + return six.unichr(html_entities.name2codepoint[text]) except (ValueError, KeyError): return match.group(0) diff --git a/docs/howto/legacy-databases.txt b/docs/howto/legacy-databases.txt index fb0f724d0ac30..a3a0bfc7471ef 100644 --- a/docs/howto/legacy-databases.txt +++ b/docs/howto/legacy-databases.txt @@ -57,13 +57,14 @@ Python declaration of each one of these models to specify it is a :attr:`managed ` one. For example, consider this generated model definition: -.. parsed-literal:: +.. code-block:: python + :emphasize-lines: 5 class Person(models.Model): id = models.IntegerField(primary_key=True) first_name = models.CharField(max_length=70) class Meta: - **managed = False** + managed = False db_table = 'CENSUS_PERSONS' If you wanted to modify existing data on your ``CENSUS_PERSONS`` SQL table diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 170afe5a77c52..d01887af20331 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -465,8 +465,9 @@ Jeremy Dunck part of a sweeping move to Python from a grab bag of half a dozen languages. He was drawn to Django's balance of practical batteries included philosophy, care and thought in code design, and strong open source - community. In addition to his current job in private progressive education, - Preston contributes some developer time to local non-profits. + community. Currently working for Amazon Web Services, he is always looking + for opportunities to volunteer for community oriented education projects, + such as for kids and scientists (e.g. Software Carpentry). Preston lives with his family and animal menagerie in Santa Barbara, CA, USA. diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 751327e6ac6e1..cf344e1702d37 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -73,8 +73,8 @@ two databases: If you're using a backend that isn't SQLite, you will need to provide other details for each database: -* The :setting:`USER` option for each of your databases needs to - specify an existing user account for the database. +* The :setting:`USER` option needs to specify an existing user account + for the database. * The :setting:`PASSWORD` option needs to provide the password for the :setting:`USER` that has been specified. diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index c91ea2d218bf7..5c1b1cab6f604 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -436,6 +436,8 @@ these changes. :ref:`initial SQL data` in ``myapp/models/sql/``. Move your custom SQL files to ``myapp/sql/``. +* The model and form ``IPAddressField`` will be removed. + * FastCGI support via the ``runfcgi`` management command will be removed. Please deploy your project using WSGI. diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 06eec90a8bec6..020fc918b8827 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -5,6 +5,7 @@ Date-based mixins .. currentmodule:: django.views.generic.dates .. note:: + All the date formatting attributes in these mixins use :func:`~time.strftime` format characters. Do not try to use the format characters from the :ttag:`now` template tag as they are not compatible. diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 0e4d352132fd2..81b65d04d4f5a 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -657,6 +657,10 @@ For each field, we describe the default widget used if you don't specify .. class:: IPAddressField(**kwargs) + .. deprecated:: 1.7 + This field has been deprecated in favour of + :class:`~django.forms.GenericIPAddressField`. + * Default widget: :class:`TextInput` * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 1e3476166f2e5..248c630437d44 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -849,6 +849,10 @@ An integer. The default form widget for this field is a .. class:: IPAddressField([**options]) +.. deprecated:: 1.7 + This field has been deprecated in favour of + :class:`~django.db.models.GenericIPAddressField`. + An IP address, in string format (e.g. "192.0.2.30"). The default form widget for this field is a :class:`~django.forms.TextInput`. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index facfe2fb267fe..4c46f3a8cdf9d 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2599,6 +2599,9 @@ your additional files directory(ies) e.g.:: "/opt/webfiles/common", ) +Note that these paths should use Unix-style forward slashes, even on Windows +(e.g. ``"C:/Users/user/mysite/extra_static_content"``). + Prefixes (optional) ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 924ec51ba9a16..573ad49dc2d91 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -92,6 +92,12 @@ The :meth:`QuerySet.as_manager() ` class method has been added to :ref:`create Manager with QuerySet methods `. +Using a custom manager when traversing reverse relations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is now possible to :ref:`specify a custom manager +` when traversing a reverse relationship. + Admin shortcuts support time zones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -105,6 +111,25 @@ In addition, the widgets now display a help message when the browser and server time zone are different, to clarify how the value inserted in the field will be interpreted. +Using database cursors as context managers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prior to Python 2.7, database cursors could be used as a context manager. The +specific backend's cursor defined the behavior of the context manager. The +behavior of magic method lookups was changed with Python 2.7 and cursors were +no longer usable as context managers. + +Django 1.7 allows a cursor to be used as a context manager that is a shortcut +for the following, instead of backend specific behavior. + +.. code-block:: python + + c = connection.cursor() + try: + c.execute(...) + finally: + c.close() + Minor features ~~~~~~~~~~~~~~ @@ -504,3 +529,12 @@ to ``utils.py`` in an effort to unify all util and utils references: ``ModelAdmin.get_formsets`` has been deprecated in favor of the new :meth:`~django.contrib.admin.ModelAdmin.get_formsets_with_inlines`, in order to better handle the case of selecting showing inlines on a ``ModelAdmin``. + +``IPAddressField`` +~~~~~~~~~~~~~~~~~~ + +The :class:`django.db.models.IPAddressField` and +:class:`django.forms.IPAddressField` fields have been deprecated in favor of +:class:`django.db.models.GenericIPAddressField` and +:class:`django.forms.GenericIPAddressField`. + diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 23f4ec7398aa9..696bddac95f17 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -1136,6 +1136,31 @@ above example code would look like this:: >>> b.entries.filter(headline__contains='Lennon') >>> b.entries.count() +.. _using-custom-reverse-manager: + +Using a custom reverse manager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +By default the :class:`~django.db.models.fields.related.RelatedManager` used +for reverse relations is a subclass of the :ref:`default manager ` +for that model. If you would like to specify a different manager for a given +query you can use the following syntax:: + + from django.db import models + + class Entry(models.Model): + #... + objects = models.Manager() # Default Manager + entries = EntryManager() # Custom Manager + + >>> b = Blog.objects.get(id=1) + >>> b.entry_set(manager='entries').all() + +Additional methods to handle related objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + In addition to the :class:`~django.db.models.query.QuerySet` methods defined in "Retrieving objects" above, the :class:`~django.db.models.ForeignKey` :class:`~django.db.models.Manager` has additional methods used to handle the diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 7437d51d28b1a..1369ed84f5f62 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -297,3 +297,30 @@ database library will automatically escape your parameters as necessary. Also note that Django expects the ``"%s"`` placeholder, *not* the ``"?"`` placeholder, which is used by the SQLite Python bindings. This is for the sake of consistency and sanity. + +.. versionchanged:: 1.7 + +:pep:`249` does not state whether a cursor should be usable as a context +manager. Prior to Python 2.7, a cursor was usable as a context manager due +an unexpected behavior in magic method lookups (`Python ticket #9220`_). +Django 1.7 explicitly added support to allow using a cursor as context +manager. + +.. _`Python ticket #9220`: http://bugs.python.org/issue9220 + +Using a cursor as a context manager: + +.. code-block:: python + + with connection.cursor() as c: + c.execute(...) + +is equivalent to: + +.. code-block:: python + + c = connection.cursor() + try: + c.execute(...) + finally: + c.close() \ No newline at end of file diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 5e6b122fecb80..99b21bd266038 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -143,6 +143,11 @@ def setUp(self): self.today = datetime.date.today() self.tomorrow = self.today + datetime.timedelta(days=1) self.one_week_ago = self.today - datetime.timedelta(days=7) + if self.today.month == 12: + self.next_month = self.today.replace(year=self.today.year + 1, month=1, day=1) + else: + self.next_month = self.today.replace(month=self.today.month + 1, day=1) + self.next_year = self.today.replace(year=self.today.year + 1, month=1, day=1) self.request_factory = RequestFactory() @@ -196,7 +201,7 @@ def test_datefieldlistfilter(self): % (self.today, self.tomorrow)) request = self.request_factory.get('/', {'date_registered__gte': self.today.replace(day=1), - 'date_registered__lt': self.tomorrow}) + 'date_registered__lt': self.next_month}) changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned @@ -214,10 +219,10 @@ def test_datefieldlistfilter(self): self.assertEqual(choice['selected'], True) self.assertEqual(choice['query_string'], '?date_registered__gte=%s' '&date_registered__lt=%s' - % (self.today.replace(day=1), self.tomorrow)) + % (self.today.replace(day=1), self.next_month)) request = self.request_factory.get('/', {'date_registered__gte': self.today.replace(month=1, day=1), - 'date_registered__lt': self.tomorrow}) + 'date_registered__lt': self.next_year}) changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned @@ -235,7 +240,7 @@ def test_datefieldlistfilter(self): self.assertEqual(choice['selected'], True) self.assertEqual(choice['query_string'], '?date_registered__gte=%s' '&date_registered__lt=%s' - % (self.today.replace(month=1, day=1), self.tomorrow)) + % (self.today.replace(month=1, day=1), self.next_year)) request = self.request_factory.get('/', {'date_registered__gte': str(self.one_week_ago), 'date_registered__lt': str(self.tomorrow)}) diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py index 741e2ed73445c..a5107d44448ec 100644 --- a/tests/aggregation_regress/tests.py +++ b/tests/aggregation_regress/tests.py @@ -393,6 +393,17 @@ def test_db_col_table(self): qs = Entries.objects.annotate(clue_count=Count('clues__ID')) self.assertQuerysetEqual(qs, []) + def test_boolean_conversion(self): + # Aggregates mixed up ordering of columns for backend's convert_values + # method. Refs #21126. + e = Entries.objects.create(Entry='foo') + c = Clues.objects.create(EntryID=e, Clue='bar') + qs = Clues.objects.select_related('EntryID').annotate(Count('ID')) + self.assertQuerysetEqual( + qs, [c], lambda x: x) + self.assertEqual(qs[0].EntryID, e) + self.assertIs(qs[0].EntryID.Exclude, False) + def test_empty(self): # Regression for #10089: Check handling of empty result sets with # aggregates diff --git a/tests/backends/tests.py b/tests/backends/tests.py index a6badac9a905e..c6fe4e98f3b86 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -15,7 +15,7 @@ from django.db.backends.signals import connection_created from django.db.backends.sqlite3.base import DatabaseOperations from django.db.backends.postgresql_psycopg2 import version as pg_version -from django.db.backends.utils import format_number +from django.db.backends.utils import format_number, CursorWrapper from django.db.models import Sum, Avg, Variance, StdDev from django.db.models.fields import (AutoField, DateField, DateTimeField, DecimalField, IntegerField, TimeField) @@ -613,6 +613,29 @@ def test_duplicate_table_error(self): with self.assertRaises(DatabaseError): cursor.execute(query) + def test_cursor_contextmanager(self): + """ + Test that cursors can be used as a context manager + """ + with connection.cursor() as cursor: + self.assertTrue(isinstance(cursor, CursorWrapper)) + # Both InterfaceError and ProgrammingError seem to be used when + # accessing closed cursor (psycopg2 has InterfaceError, rest seem + # to use ProgrammingError). + with self.assertRaises(connection.features.closed_cursor_error_class): + # cursor should be closed, so no queries should be possible. + cursor.execute("select 1") + + @unittest.skipUnless(connection.vendor == 'postgresql', + "Psycopg2 specific cursor.closed attribute needed") + def test_cursor_contextmanager_closing(self): + # There isn't a generic way to test that cursors are closed, but + # psycopg2 offers us a way to check that by closed attribute. + # So, run only on psycopg2 for that reason. + with connection.cursor() as cursor: + self.assertTrue(isinstance(cursor, CursorWrapper)) + self.assertTrue(cursor.closed) + # We don't make these tests conditional because that means we would need to # check and differentiate between: diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index cba375f4d7cc8..26d848b7c0839 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -11,6 +11,7 @@ from __future__ import unicode_literals +from django.contrib.contenttypes import generic from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -63,12 +64,28 @@ def manager_only(self): CustomManager = BaseCustomManager.from_queryset(CustomQuerySet) +class FunPeopleManager(models.Manager): + def get_queryset(self): + return super(FunPeopleManager, self).get_queryset().filter(fun=True) + +class BoringPeopleManager(models.Manager): + def get_queryset(self): + return super(BoringPeopleManager, self).get_queryset().filter(fun=False) + @python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) fun = models.BooleanField(default=False) + + favorite_book = models.ForeignKey('Book', null=True, related_name='favorite_books') + favorite_thing_type = models.ForeignKey('contenttypes.ContentType', null=True) + favorite_thing_id = models.IntegerField(null=True) + favorite_thing = generic.GenericForeignKey('favorite_thing_type', 'favorite_thing_id') + objects = PersonManager() + fun_people = FunPeopleManager() + boring_people = BoringPeopleManager() custom_queryset_default_manager = CustomQuerySet.as_manager() custom_queryset_custom_manager = CustomManager('hello') @@ -84,6 +101,9 @@ class Book(models.Model): published_objects = PublishedBookManager() authors = models.ManyToManyField(Person, related_name='books') + favorite_things = generic.GenericRelation(Person, + content_type_field='favorite_thing_type', object_id_field='favorite_thing_id') + def __str__(self): return self.title diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index ff14ad8439682..f9a9f33d87852 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -7,10 +7,15 @@ class CustomManagerTests(TestCase): - def test_manager(self): - Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) - p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + def setUp(self): + self.b1 = Book.published_objects.create( + title="How to program", author="Rodney Dangerfield", is_published=True) + self.b2 = Book.published_objects.create( + title="How to be smart", author="Albert Einstein", is_published=False) + self.p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + self.p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + def test_manager(self): # Test a custom `Manager` method. self.assertQuerysetEqual( Person.objects.get_fun_people(), [ @@ -61,14 +66,8 @@ def test_manager(self): # The RelatedManager used on the 'books' descriptor extends the default # manager - self.assertIsInstance(p2.books, PublishedBookManager) + self.assertIsInstance(self.p2.books, PublishedBookManager) - Book.published_objects.create( - title="How to program", author="Rodney Dangerfield", is_published=True - ) - b2 = Book.published_objects.create( - title="How to be smart", author="Albert Einstein", is_published=False - ) # The default manager, "objects", doesn't exist, because a custom one # was provided. @@ -76,7 +75,7 @@ def test_manager(self): # The RelatedManager used on the 'authors' descriptor extends the # default manager - self.assertIsInstance(b2.authors, PersonManager) + self.assertIsInstance(self.b2.authors, PersonManager) self.assertQuerysetEqual( Book.published_objects.all(), [ @@ -114,3 +113,79 @@ def test_manager(self): ], lambda c: c.name ) + + def test_related_manager_fk(self): + self.p1.favorite_book = self.b1 + self.p1.save() + self.p2.favorite_book = self.b1 + self.p2.save() + + self.assertQuerysetEqual( + self.b1.favorite_books.order_by('first_name').all(), [ + "Bugs", + "Droopy", + ], + lambda c: c.first_name + ) + self.assertQuerysetEqual( + self.b1.favorite_books(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name + ) + self.assertQuerysetEqual( + self.b1.favorite_books(manager='fun_people').all(), [ + "Bugs", + ], + lambda c: c.first_name + ) + + def test_related_manager_gfk(self): + self.p1.favorite_thing = self.b1 + self.p1.save() + self.p2.favorite_thing = self.b1 + self.p2.save() + + self.assertQuerysetEqual( + self.b1.favorite_things.order_by('first_name').all(), [ + "Bugs", + "Droopy", + ], + lambda c: c.first_name + ) + self.assertQuerysetEqual( + self.b1.favorite_things(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name + ) + self.assertQuerysetEqual( + self.b1.favorite_things(manager='fun_people').all(), [ + "Bugs", + ], + lambda c: c.first_name + ) + + def test_related_manager_m2m(self): + self.b1.authors.add(self.p1) + self.b1.authors.add(self.p2) + + self.assertQuerysetEqual( + self.b1.authors.order_by('first_name').all(), [ + "Bugs", + "Droopy", + ], + lambda c: c.first_name + ) + self.assertQuerysetEqual( + self.b1.authors(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name + ) + self.assertQuerysetEqual( + self.b1.authors(manager='fun_people').all(), [ + "Bugs", + ], + lambda c: c.first_name + ) diff --git a/tests/defaultfilters/tests.py b/tests/defaultfilters/tests.py index d55babc7e9a9f..4251e754c5201 100644 --- a/tests/defaultfilters/tests.py +++ b/tests/defaultfilters/tests.py @@ -341,6 +341,24 @@ def test_urlize(self): self.assertEqual(urlize('http://[2001:db8:cafe::2]/api/9'), 'http://[2001:db8:cafe::2]/api/9') + # Check urlize correctly include quotation marks in links - #20364 + self.assertEqual(urlize('before "hi@example.com" afterwards'), + 'before "hi@example.com" afterwards') + self.assertEqual(urlize('before hi@example.com" afterwards'), + 'before hi@example.com" afterwards') + self.assertEqual(urlize('before "hi@example.com afterwards'), + 'before "hi@example.com afterwards') + self.assertEqual(urlize('before \'hi@example.com\' afterwards'), + 'before \'hi@example.com\' afterwards') + self.assertEqual(urlize('before hi@example.com\' afterwards'), + 'before hi@example.com\' afterwards') + self.assertEqual(urlize('before \'hi@example.com afterwards'), + 'before \'hi@example.com afterwards') + + # Check urlize copes with commas following URLs in quotes - see #20364 + self.assertEqual(urlize('Email us at "hi@example.com", or phone us at +xx.yy'), + 'Email us at "hi@example.com", or phone us at +xx.yy') + def test_wordcount(self): self.assertEqual(wordcount(''), 0) self.assertEqual(wordcount('oneword'), 1) diff --git a/tests/field_deconstruction/tests.py b/tests/field_deconstruction/tests.py index 4ccf4d048cfcf..ba09671a1e41b 100644 --- a/tests/field_deconstruction/tests.py +++ b/tests/field_deconstruction/tests.py @@ -1,3 +1,4 @@ +import warnings from django.test import TestCase from django.db import models @@ -173,7 +174,9 @@ def test_integer_field(self): self.assertEqual(kwargs, {}) def test_ip_address_field(self): - field = models.IPAddressField() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + field = models.IPAddressField() name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.IPAddressField") self.assertEqual(args, []) diff --git a/tests/forms_tests/tests/test_error_messages.py b/tests/forms_tests/tests/test_error_messages.py index a884bdc75bef7..2cfe6f3e36e12 100644 --- a/tests/forms_tests/tests/test_error_messages.py +++ b/tests/forms_tests/tests/test_error_messages.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import warnings + from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.test import TestCase @@ -192,7 +194,9 @@ def test_ipaddressfield(self): 'required': 'REQUIRED', 'invalid': 'INVALID IP ADDRESS', } - f = IPAddressField(error_messages=e) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + f = IPAddressField(error_messages=e) self.assertFormErrors(['REQUIRED'], f.clean, '') self.assertFormErrors(['INVALID IP ADDRESS'], f.clean, '127.0.0') diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index d0409155dd5dc..136d11383f698 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime +import warnings from django.forms import * from django.forms.extras import SelectDateWidget @@ -535,7 +536,9 @@ class ComplexFieldForm(Form): self.assertEqual(f.cleaned_data['field1'], 'some text,JP,2007-04-25 06:24:00') def test_ipaddress(self): - f = IPAddressField() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + f = IPAddressField() self.assertFormErrors(['This field is required.'], f.clean, '') self.assertFormErrors(['This field is required.'], f.clean, None) self.assertEqual(f.clean(' 127.0.0.1'), '127.0.0.1') @@ -544,7 +547,9 @@ def test_ipaddress(self): self.assertFormErrors(['Enter a valid IPv4 address.'], f.clean, '1.2.3.4.5') self.assertFormErrors(['Enter a valid IPv4 address.'], f.clean, '256.125.1.5') - f = IPAddressField(required=False) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + f = IPAddressField(required=False) self.assertEqual(f.clean(''), '') self.assertEqual(f.clean(None), '') self.assertEqual(f.clean(' 127.0.0.1'), '127.0.0.1') diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py index 033bc7a035998..2d667791e89eb 100644 --- a/tests/forms_tests/tests/test_widgets.py +++ b/tests/forms_tests/tests/test_widgets.py @@ -695,6 +695,37 @@ class CustomRadioSelect(RadioSelect):
  • +
""") + + def test_nested_choices(self): + # Choices can be nested for radio buttons: + w = RadioSelect() + w.choices=(('unknown', 'Unknown'), ('Audio', (('vinyl', 'Vinyl'), ('cd', 'CD'))), ('Video', (('vhs', 'VHS'), ('dvd', 'DVD')))) + self.assertHTMLEqual(w.render('nestchoice', 'dvd', attrs={'id':'media'}), """
    +
  • +
  • Audio
      +
    • +
    • +
  • +
  • Video
      +
    • +
    • +
  • +
""") + + # Choices can be nested for checkboxes: + w = CheckboxSelectMultiple() + w.choices=(('unknown', 'Unknown'), ('Audio', (('vinyl', 'Vinyl'), ('cd', 'CD'))), ('Video', (('vhs', 'VHS'), ('dvd', 'DVD')))) + self.assertHTMLEqual(w.render('nestchoice', ('vinyl', 'dvd'), attrs={'id':'media'}), """
    +
  • +
  • Audio
      +
    • +
    • +
  • +
  • Video
      +
    • +
    • +
""") def test_checkboxselectmultiple(self): diff --git a/tests/inspectdb/models.py b/tests/inspectdb/models.py index c25202f248f24..83cd906a07087 100644 --- a/tests/inspectdb/models.py +++ b/tests/inspectdb/models.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals +import warnings from django.db import models @@ -49,7 +50,9 @@ class ColumnTypes(models.Model): file_path_field = models.FilePathField() float_field = models.FloatField() int_field = models.IntegerField() - ip_address_field = models.IPAddressField() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ip_address_field = models.IPAddressField() gen_ip_adress_field = models.GenericIPAddressField(protocol="ipv4") pos_int_field = models.PositiveIntegerField() pos_small_int_field = models.PositiveSmallIntegerField() diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 1f247c1a97963..723f1ce7411a7 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -16,6 +16,16 @@ def set_up_test_model(self, app_label, second_model=False): """ Creates a test model state and database table. """ + # Delete the tables if they already exist + cursor = connection.cursor() + try: + cursor.execute("DROP TABLE %s_pony" % app_label) + except: + pass + try: + cursor.execute("DROP TABLE %s_stable" % app_label) + except: + pass # Make the "current" state operations = [migrations.CreateModel( "Pony", @@ -332,6 +342,15 @@ def test_run_python(self): # And test reversal fails with self.assertRaises(NotImplementedError): operation.database_backwards("test_runpython", None, new_state, project_state) + # Now test we can do it with a callable + def inner_method(models, schema_editor): + Pony = models.get_model("test_runpython", "Pony") + Pony.objects.create(pink=1, weight=3.55) + Pony.objects.create(weight=5) + operation = migrations.RunPython(inner_method) + with connection.schema_editor() as editor: + operation.database_forwards("test_runpython", editor, project_state, new_state) + self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 4) class MigrateNothingRouter(object): diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index 8c304cc7265fa..a85dfc4f04251 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -1,5 +1,6 @@ import os import tempfile +import warnings from django.core.exceptions import ImproperlyConfigured @@ -85,7 +86,9 @@ class VerboseNameField(models.Model): # Don't want to depend on Pillow/PIL in this test #field_image = models.ImageField("verbose field") field12 = models.IntegerField("verbose field12") - field13 = models.IPAddressField("verbose field13") + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + field13 = models.IPAddressField("verbose field13") field14 = models.GenericIPAddressField("verbose field14", protocol="ipv4") field15 = models.NullBooleanField("verbose field15") field16 = models.PositiveIntegerField("verbose field16") diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index e4cc880bd6dd6..ac7dd0252f73a 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -3,6 +3,7 @@ import datetime from decimal import Decimal import unittest +import warnings from django import test from django import forms @@ -603,9 +604,11 @@ def test_IntegerField(self): def test_IPAddressField(self): lazy_func = lazy(lambda: '127.0.0.1', six.text_type) - self.assertIsInstance( - IPAddressField().get_prep_value(lazy_func()), - six.text_type) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + self.assertIsInstance( + IPAddressField().get_prep_value(lazy_func()), + six.text_type) def test_GenericIPAddressField(self): lazy_func = lazy(lambda: '127.0.0.1', six.text_type) diff --git a/tests/serializers_regress/models.py b/tests/serializers_regress/models.py index ab3d3063a4461..52bc8935adb79 100644 --- a/tests/serializers_regress/models.py +++ b/tests/serializers_regress/models.py @@ -4,6 +4,7 @@ This class sets up a model for each model field type (except for image types, because of the Pillow/PIL dependency). """ +import warnings from django.db import models from django.contrib.contenttypes import generic @@ -52,7 +53,9 @@ class BigIntegerData(models.Model): # data = models.ImageField(null=True) class IPAddressData(models.Model): - data = models.IPAddressField(null=True) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + data = models.IPAddressField(null=True) class GenericIPAddressData(models.Model): data = models.GenericIPAddressField(null=True) @@ -199,7 +202,9 @@ class IntegerPKData(models.Model): # data = models.ImageField(primary_key=True) class IPAddressPKData(models.Model): - data = models.IPAddressField(primary_key=True) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + data = models.IPAddressField(primary_key=True) class GenericIPAddressPKData(models.Model): data = models.GenericIPAddressField(primary_key=True) diff --git a/tests/string_lookup/models.py b/tests/string_lookup/models.py index a2d64cd0b2d66..43ed90a462e32 100644 --- a/tests/string_lookup/models.py +++ b/tests/string_lookup/models.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import warnings from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -49,7 +50,9 @@ def __str__(self): class Article(models.Model): name = models.CharField(max_length=50) text = models.TextField() - submitted_from = models.IPAddressField(blank=True, null=True) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + submitted_from = models.IPAddressField(blank=True, null=True) def __str__(self): return "Article %s" % self.name diff --git a/tests/utils_tests/test_text.py b/tests/utils_tests/test_text.py index c9dde6b1b3651..dd72d26e06446 100644 --- a/tests/utils_tests/test_text.py +++ b/tests/utils_tests/test_text.py @@ -106,3 +106,16 @@ def test_slugify(self): ) for value, output in items: self.assertEqual(text.slugify(value), output) + + def test_unescape_entities(self): + items = [ + ('', ''), + ('foo', 'foo'), + ('&', '&'), + ('&', '&'), + ('&', '&'), + ('foo & bar', 'foo & bar'), + ('foo & bar', 'foo & bar'), + ] + for value, output in items: + self.assertEqual(text.unescape_entities(value), output)