diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-07-22 19:04:25 +0100 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-07-22 19:04:25 +0100 |
| commit | 03ec3219a04093da6c2e82436cee5c451df4f8aa (patch) | |
| tree | 69e3cc2857b7bc41f269363b0056b361129da118 /django | |
| parent | 3a6580e485c11fc5502ffd1bb53347459db43421 (diff) | |
| parent | 29a09b3638e625458b5e07ed0956451432421acf (diff) | |
Merge branch 'master' into schema-alteration
Conflicts:
django/db/backends/mysql/introspection.py
django/db/backends/oracle/creation.py
django/db/backends/postgresql_psycopg2/creation.py
django/db/models/base.py
django/db/models/loading.py
Diffstat (limited to 'django')
114 files changed, 979 insertions, 642 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 7882b21b9b..364aa10320 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -184,6 +184,7 @@ EMAIL_PORT = 25 EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' EMAIL_USE_TLS = False +EMAIL_USE_SSL = False # List of strings representing installed apps. INSTALLED_APPS = () diff --git a/django/conf/locale/__init__.py b/django/conf/locale/__init__.py index df6ab07663..59927b2397 100644 --- a/django/conf/locale/__init__.py +++ b/django/conf/locale/__init__.py @@ -279,7 +279,7 @@ LANG_INFO = { 'bidi': False, 'code': 'lt', 'name': 'Lithuanian', - 'name_local': 'Lithuanian', + 'name_local': 'Lietuviškai', }, 'lv': { 'bidi': False, @@ -399,7 +399,7 @@ LANG_INFO = { 'bidi': False, 'code': 'sq', 'name': 'Albanian', - 'name_local': 'Albanian', + 'name_local': 'shqip', }, 'sr': { 'bidi': False, @@ -441,7 +441,7 @@ LANG_INFO = { 'bidi': False, 'code': 'th', 'name': 'Thai', - 'name_local': 'Thai', + 'name_local': 'ภาษาไทย', }, 'tr': { 'bidi': False, @@ -477,7 +477,7 @@ LANG_INFO = { 'bidi': False, 'code': 'vi', 'name': 'Vietnamese', - 'name_local': 'Vietnamese', + 'name_local': 'Tiếng Việt', }, 'zh-cn': { 'bidi': False, diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po index 371f0af2ab..f8069f07d2 100644 --- a/django/conf/locale/en/LC_MESSAGES/django.po +++ b/django/conf/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-25 14:27+0200\n" +"POT-Creation-Date: 2013-06-11 18:44+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English <en@li.org>\n" @@ -699,11 +699,22 @@ msgstr "" msgid "Enter a list of values." msgstr "" -#: forms/forms.py:158 +#. Translators: This is the default suffix added to form field labels +#: forms/forms.py:90 +msgid ":" +msgstr "" + +#: forms/forms.py:159 #, python-format msgid "(Hidden field %(name)s) %(error)s" msgstr "" +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: forms/forms.py:525 +msgid ":?.!" +msgstr "" + #: forms/formsets.py:310 #, python-format msgid "Please submit %d or fewer forms." diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 320d3267a7..3ffb85e6c6 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -266,10 +266,12 @@ class InlineAdminForm(AdminForm): yield InlineFieldset(self.formset, self.form, name, self.readonly_fields, model_admin=self.model_admin, **options) - def has_auto_field(self): - if self.form._meta.model._meta.has_auto_field: + def needs_explicit_pk_field(self): + # Auto fields are editable (oddly), so need to check for auto or non-editable pk + if self.form._meta.model._meta.has_auto_field or not self.form._meta.model._meta.pk.editable: return True - # Also search any parents for an auto field. + # Also search any parents for an auto field. (The pk info is propagated to child + # models so that does not need to be checked in parents.) for parent in self.form._meta.model._meta.get_parent_list(): if parent._meta.has_auto_field: return True diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 9c92f7ae9c..afc7cfc5bd 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -771,12 +771,10 @@ class ModelAdmin(BaseModelAdmin): Returns the preserved filters querystring. """ - # FIXME: We can remove that getattr as soon as #20619 is fixed. - match = getattr(request, 'resolver_match', None) - + match = request.resolver_match if self.preserve_filters and match: opts = self.model._meta - current_url = '%s:%s' % (match.namespace, match.url_name) + current_url = '%s:%s' % (match.app_name, match.url_name) changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name) if current_url == changelist_url: preserved_filters = request.GET.urlencode() diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index e0f43dfbfe..8abf49331b 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -439,6 +439,7 @@ class AdminSite(object): context = { 'title': _('%s administration') % capfirst(app_label), 'app_list': [app_dict], + 'app_label': app_label, } context.update(extra_context or {}) diff --git a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js index 671af9bd78..aa12f8cf01 100644 --- a/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/static/admin/js/admin/DateTimeShortcuts.js @@ -14,6 +14,8 @@ var DateTimeShortcuts = { clockDivName: 'clockbox', // name of clock <div> that gets toggled clockLinkName: 'clocklink', // name of the link that is used to toggle shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts + timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch + timezoneOffset: 0, admin_media_prefix: '', init: function() { // Get admin_media_prefix by grabbing it off the window object. It's @@ -26,17 +28,77 @@ var DateTimeShortcuts = { DateTimeShortcuts.admin_media_prefix = '/missing-admin-media-prefix/'; } + if (window.__admin_utc_offset__ != undefined) { + var serverOffset = window.__admin_utc_offset__; + var localOffset = new Date().getTimezoneOffset() * -60; + DateTimeShortcuts.timezoneOffset = localOffset - serverOffset; + } + var inputs = document.getElementsByTagName('input'); for (i=0; i<inputs.length; i++) { var inp = inputs[i]; if (inp.getAttribute('type') == 'text' && inp.className.match(/vTimeField/)) { DateTimeShortcuts.addClock(inp); + DateTimeShortcuts.addTimezoneWarning(inp); } else if (inp.getAttribute('type') == 'text' && inp.className.match(/vDateField/)) { DateTimeShortcuts.addCalendar(inp); + DateTimeShortcuts.addTimezoneWarning(inp); } } }, + // Return the current time while accounting for the server timezone. + now: function() { + if (window.__admin_utc_offset__ != undefined) { + var serverOffset = window.__admin_utc_offset__; + var localNow = new Date(); + var localOffset = localNow.getTimezoneOffset() * -60; + localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset)); + return localNow; + } else { + return new Date(); + } + }, + // Add a warning when the time zone in the browser and backend do not match. + addTimezoneWarning: function(inp) { + var $ = django.jQuery; + var warningClass = DateTimeShortcuts.timezoneWarningClass; + var timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600; + + // Only warn if there is a time zone mismatch. + if (!timezoneOffset) + return; + + // Check if warning is already there. + if ($(inp).siblings('.' + warningClass).length) + return; + + var message; + if (timezoneOffset > 0) { + message = ngettext( + 'Note: You are %s hour ahead of server time.', + 'Note: You are %s hours ahead of server time.', + timezoneOffset + ); + } + else { + timezoneOffset *= -1 + message = ngettext( + 'Note: You are %s hour behind server time.', + 'Note: You are %s hours behind server time.', + timezoneOffset + ); + } + message = interpolate(message, [timezoneOffset]); + + var $warning = $('<span>'); + $warning.attr('class', warningClass); + $warning.text(message); + + $(inp).parent() + .append($('<br>')) + .append($warning) + }, // Add clock widget to a given field addClock: function(inp) { var num = DateTimeShortcuts.clockInputs.length; @@ -48,7 +110,7 @@ var DateTimeShortcuts = { shortcuts_span.className = DateTimeShortcuts.shortCutsClass; inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); var now_link = document.createElement('a'); - now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + get_format('TIME_INPUT_FORMATS')[0] + "'));"); + now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);"); now_link.appendChild(document.createTextNode(gettext('Now'))); var clock_link = document.createElement('a'); clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');'); @@ -84,11 +146,10 @@ var DateTimeShortcuts = { quickElement('h2', clock_box, gettext('Choose a time')); var time_list = quickElement('ul', clock_box, ''); time_list.className = 'timelist'; - var time_format = get_format('TIME_INPUT_FORMATS')[0]; - quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));"); - quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));"); - quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));"); - quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));"); + quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", -1);"); + quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 0);"); + quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 6);"); + quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", 12);"); var cancel_p = quickElement('p', clock_box, ''); cancel_p.className = 'calendar-cancel'; @@ -128,7 +189,14 @@ var DateTimeShortcuts = { removeEvent(document, 'click', DateTimeShortcuts.dismissClockFunc[num]); }, handleClockQuicklink: function(num, val) { - DateTimeShortcuts.clockInputs[num].value = val; + var d; + if (val == -1) { + d = DateTimeShortcuts.now(); + } + else { + d = new Date(1970, 1, 1, val, 0, 0, 0) + } + DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]); DateTimeShortcuts.clockInputs[num].focus(); DateTimeShortcuts.dismissClock(num); }, @@ -258,7 +326,7 @@ var DateTimeShortcuts = { DateTimeShortcuts.calendars[num].drawNextMonth(); }, handleCalendarCallback: function(num) { - format = get_format('DATE_INPUT_FORMATS')[0]; + var format = get_format('DATE_INPUT_FORMATS')[0]; // the format needs to be escaped a little format = format.replace('\\', '\\\\'); format = format.replace('\r', '\\r'); @@ -276,7 +344,7 @@ var DateTimeShortcuts = { ").style.display='none';}"].join(''); }, handleCalendarQuickLink: function(num, offset) { - var d = new Date(); + var d = DateTimeShortcuts.now(); d.setDate(d.getDate() + offset) DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]); DateTimeShortcuts.calendarInputs[num].focus(); diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js index 6d1748663e..c82c8c05cb 100644 --- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js +++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js @@ -55,7 +55,7 @@ function dismissRelatedLookupPopup(win, chosenId) { function showAddAnotherPopup(triggeringLink) { var name = triggeringLink.id.replace(/^add_/, ''); name = id_to_windowname(name); - href = triggeringLink.href + var href = triggeringLink.href; if (href.indexOf('?') == -1) { href += '?_popup=1'; } else { @@ -73,10 +73,11 @@ function dismissAddAnotherPopup(win, newId, newRepr) { newRepr = html_unescape(newRepr); var name = windowname_to_id(win.name); var elem = document.getElementById(name); + var o; if (elem) { var elemName = elem.nodeName.toUpperCase(); if (elemName == 'SELECT') { - var o = new Option(newRepr, newId); + o = new Option(newRepr, newId); elem.options[elem.options.length] = o; o.selected = true; } else if (elemName == 'INPUT') { @@ -88,8 +89,7 @@ function dismissAddAnotherPopup(win, newId, newRepr) { } } else { var toId = name + "_to"; - elem = document.getElementById(toId); - var o = new Option(newRepr, newId); + o = new Option(newRepr, newId); SelectBox.add_to_cache(toId, o); SelectBox.redisplay(toId); } diff --git a/django/contrib/admin/templates/admin/app_index.html b/django/contrib/admin/templates/admin/app_index.html index 9f65357292..7aff935126 100644 --- a/django/contrib/admin/templates/admin/app_index.html +++ b/django/contrib/admin/templates/admin/app_index.html @@ -1,6 +1,8 @@ {% extends "admin/index.html" %} {% load i18n %} +{% block bodyclass %}app-{{ app_label }} {{ block.super }}{% endblock %} + {% if not is_popup %} {% block breadcrumbs %} <div class="breadcrumbs"> diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 63d4419fae..9c79a4e698 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -7,6 +7,7 @@ <!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="{% block stylesheet_ie %}{% static "admin/css/ie.css" %}{% endblock %}" /><![endif]--> {% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}" />{% endif %} <script type="text/javascript">window.__admin_media_prefix__ = "{% filter escapejs %}{% static "admin/" %}{% endfilter %}";</script> +<script type="text/javascript">window.__admin_utc_offset__ = "{% filter escapejs %}{% now "Z" %}{% endfilter %}";</script> {% block extrahead %}{% endblock %} {% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %} </head> diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index d4935ed781..e35c7328f4 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -10,7 +10,7 @@ {% block coltype %}colM{% endblock %} -{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} +{% block bodyclass %}app-{{ opts.app_label }} model-{{ opts.object_name.lower }} change-form{% endblock %} {% if not is_popup %} {% block breadcrumbs %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 15583d1a8c..ce5c5b37a4 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -32,7 +32,7 @@ {% endif %}{% endif %} {% endblock %} -{% block bodyclass %}change-list{% endblock %} +{% block bodyclass %}app-{{ opts.app_label }} model-{{ opts.object_name.lower }} change-list{% endblock %} {% if not is_popup %} {% block breadcrumbs %} diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 3ed60807b7..2e94b61485 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -1,6 +1,8 @@ {% extends "admin/base_site.html" %} {% load i18n admin_urls %} +{% block bodyclass %}app-{{ opts.app_label }} model-{{ opts.object_name.lower }} delete-confirmation{% endblock %} + {% block breadcrumbs %} <div class="breadcrumbs"> <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html index 36527dcbf5..a80f4e4a54 100644 --- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html @@ -1,6 +1,8 @@ {% extends "admin/base_site.html" %} {% load i18n l10n admin_urls %} +{% block bodyclass %}app-{{ opts.app_label }} model-{{ opts.object_name.lower }} delete-confirmation delete-selected-confirmation{% endblock %} + {% block breadcrumbs %} <div class="breadcrumbs"> <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html index 2025dd8083..32917756b3 100644 --- a/django/contrib/admin/templates/admin/edit_inline/stacked.html +++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html @@ -13,7 +13,7 @@ {% for fieldset in inline_admin_form %} {% include "admin/includes/fieldset.html" %} {% endfor %} - {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} + {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} {{ inline_admin_form.fk_field.field }} </div>{% endfor %} </div> diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index ca853bc989..e6097f0dce 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -29,7 +29,7 @@ {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %} {% if inline_admin_form.show_url %}<a href="{% url 'admin:view_on_site' inline_admin_form.original_content_type_id inline_admin_form.original.pk %}">{% trans "View on site" %}</a>{% endif %} </p>{% endif %} - {% if inline_admin_form.has_auto_field %}{{ inline_admin_form.pk_field.field }}{% endif %} + {% if inline_admin_form.needs_explicit_pk_field %}{{ inline_admin_form.pk_field.field }}{% endif %} {{ inline_admin_form.fk_field.field }} {% spaceless %} {% for fieldset in inline_admin_form %} diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py index 19da87d61f..5d9c5b5427 100644 --- a/django/contrib/admin/templatetags/admin_urls.py +++ b/django/contrib/admin/templatetags/admin_urls.py @@ -38,7 +38,7 @@ def add_preserved_filters(context, url, popup=False): except Resolver404: pass else: - current_url = '%s:%s' % (match.namespace, match.url_name) + current_url = '%s:%s' % (match.app_name, match.url_name) changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name) if changelist_url == current_url and '_changelist_filters' in preserved_filters: preserved_filters = dict(parse_qsl(preserved_filters['_changelist_filters'])) diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index 5a1db68613..ca660606e5 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -70,10 +70,7 @@ class UserAdmin(admin.ModelAdmin): """ defaults = {} if obj is None: - defaults.update({ - 'form': self.add_form, - 'fields': admin.util.flatten_fieldsets(self.add_fieldsets), - }) + defaults['form'] = self.add_form defaults.update(kwargs) return super(UserAdmin, self).get_form(request, obj, **defaults) diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 05e9bfd721..6b31f72b03 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Permission class ModelBackend(object): """ - Authenticates against django.contrib.auth.models.User. + Authenticates against settings.AUTH_USER_MODEL. """ def authenticate(self, username=None, password=None, **kwargs): diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 87e4218a8f..7656b50437 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -22,6 +22,7 @@ UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUS HASHERS = None # lazily loaded from PASSWORD_HASHERS PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS + @receiver(setting_changed) def reset_hashers(**kwargs): if kwargs['setting'] == 'PASSWORD_HASHERS': @@ -34,7 +35,7 @@ def is_password_usable(encoded): if encoded is None or encoded.startswith(UNUSABLE_PASSWORD_PREFIX): return False try: - hasher = identify_hasher(encoded) + identify_hasher(encoded) except ValueError: return False return True @@ -48,7 +49,7 @@ def check_password(password, encoded, setter=None, preferred='default'): If setter is specified, it'll be called when you need to regenerate the password. """ - if not is_password_usable(encoded): + if password is None or not is_password_usable(encoded): return False preferred = get_hasher(preferred) diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py index 8e8119b741..4b90127c37 100644 --- a/django/contrib/auth/tests/test_hashers.py +++ b/django/contrib/auth/tests/test_hashers.py @@ -187,6 +187,13 @@ class TestUtilsHashPass(unittest.TestCase): # This might fail one day due to a hash collision. self.assertNotEqual(encoded, make_password(None), "Random password collision?") + def test_unspecified_password(self): + """ + Makes sure specifying no plain password with a valid encoded password + returns `False`. + """ + self.assertFalse(check_password(None, make_password('lètmein'))) + def test_bad_algorithm(self): with self.assertRaises(ValueError): make_password('lètmein', hasher='lolcat') diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 04d4894b8a..6f120f82e0 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -66,7 +66,7 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): if obj is not None: return ContentType.objects.db_manager(obj._state.db).get_for_model( obj, for_concrete_model=self.for_concrete_model) - elif id: + elif id is not None: return ContentType.objects.db_manager(using).get_for_id(id) else: # This should never happen. I love comments like this, don't you? @@ -130,7 +130,7 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): # performance when dealing with GFKs in loops and such. f = self.model._meta.get_field(self.ct_field) ct_id = getattr(instance, f.get_attname(), None) - if ct_id: + if ct_id is not None: ct = self.get_content_type(id=ct_id, using=instance._state.db) try: rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field)) diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 84217c331b..628018320b 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -78,7 +78,7 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations): name = 'oracle' oracle = True - valid_aggregates = dict([(a, None) for a in ('Union', 'Extent')]) + valid_aggregates = {'Union', 'Extent'} Adapter = OracleSpatialAdapter Adaptor = Adapter # Backwards-compatibility alias. diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 84dbda3239..3863bea992 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -56,6 +56,7 @@ class PostGISSphereDistance(PostGISDistance): class PostGISRelate(PostGISFunctionParam): "For PostGIS Relate(<geom>, <pattern>) calls." pattern_regex = re.compile(r'^[012TF\*]{9}$') + def __init__(self, prefix, pattern): if not self.pattern_regex.match(pattern): raise ValueError('Invalid intersection matrix pattern "%s".' % pattern) @@ -68,8 +69,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): postgis = True geom_func_prefix = 'ST_' version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') - valid_aggregates = dict([(k, None) for k in - ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')]) + valid_aggregates = {'Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union'} Adapter = PostGISAdapter Adaptor = Adapter # Backwards-compatibility alias. diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 4281cafa23..3cb2004dfd 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -56,7 +56,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): name = 'spatialite' spatialite = True version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') - valid_aggregates = dict([(k, None) for k in ('Extent', 'Union')]) + valid_aggregates = {'Extent', 'Union'} Adapter = SpatiaLiteAdapter Adaptor = Adapter # Backwards-compatibility alias. @@ -169,6 +169,7 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): Checks if the given aggregate name is supported (that is, if it's in `self.valid_aggregates`). """ + super(SpatiaLiteOperations, self).check_aggregate_support(aggregate) agg_name = aggregate.__class__.__name__ return agg_name in self.valid_aggregates diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index c89912b2d9..4199d19a88 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -140,7 +140,7 @@ class GeoQuerySet(QuerySet): Returns a GeoJSON representation of the geomtry field in a `geojson` attribute on each element of the GeoQuerySet. - The `crs` and `bbox` keywords may be set to True if the users wants + The `crs` and `bbox` keywords may be set to True if the user wants the coordinate reference system and the bounding box to be included in the GeoJSON representation of the geometry. """ diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 0d75620b4e..d75e8cd288 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -198,14 +198,6 @@ class OGRGeometry(GDALBase): def _get_coord_dim(self): "Returns the coordinate dimension of the Geometry." - if isinstance(self, GeometryCollection) and GDAL_VERSION < (1, 5, 2): - # On GDAL versions prior to 1.5.2, there exists a bug in which - # the coordinate dimension of geometry collections is always 2: - # http://trac.osgeo.org/gdal/ticket/2334 - # Here we workaround by returning the coordinate dimension of the - # first geometry in the collection instead. - if len(self): - return capi.get_coord_dim(capi.get_geom_ref(self.ptr, 0)) return capi.get_coord_dim(self.ptr) def _set_coord_dim(self, dim): diff --git a/django/contrib/gis/management/commands/ogrinspect.py b/django/contrib/gis/management/commands/ogrinspect.py index 44513e3463..2a29fe672a 100644 --- a/django/contrib/gis/management/commands/ogrinspect.py +++ b/django/contrib/gis/management/commands/ogrinspect.py @@ -53,7 +53,7 @@ class Command(LabelCommand): make_option('--multi-geom', action='store_true', dest='multi_geom', default=False, help='Treat the geometry in the data source as a geometry collection.'), make_option('--name-field', dest='name_field', - help='Specifies a field name to return for the `__unicode__` function.'), + help='Specifies a field name to return for the `__unicode__`/`__str__` function.'), make_option('--no-imports', action='store_false', dest='imports', default=True, help='Do not include `from django.contrib.gis.db import models` ' 'statement.'), diff --git a/django/contrib/gis/maps/google/gmap.py b/django/contrib/gis/maps/google/gmap.py index 75b285ca76..ff6562696d 100644 --- a/django/contrib/gis/maps/google/gmap.py +++ b/django/contrib/gis/maps/google/gmap.py @@ -134,7 +134,8 @@ class GoogleMap(object): @property def scripts(self): "Returns all <script></script> tags required with Google Maps JavaScript." - return format_html('%s\n <script type="text/javascript">\n//<![CDATA[\n%s//]]>\n </script>', self.api_script, mark_safe(self.js)) + return format_html('{0}\n <script type="text/javascript">\n//<![CDATA[\n{1}//]]>\n </script>', + self.api_script, mark_safe(self.js)) @property def style(self): diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 1703f3f1b3..e69de29bb2 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,30 +0,0 @@ -def geo_apps(): - """ - Returns a list of GeoDjango test applications that reside in - `django.contrib.gis.tests` that can be used with the current - database and the spatial libraries that are installed. - """ - from django.db import connection - from django.contrib.gis.geos import GEOS_PREPARE - from django.contrib.gis.gdal import HAS_GDAL - - apps = ['geoapp', 'relatedapp'] - - # No distance queries on MySQL. - if not connection.ops.mysql: - apps.append('distapp') - - # Test geography support with PostGIS 1.5+. - if connection.ops.postgis and connection.ops.geography: - apps.append('geogapp') - - # The following GeoDjango test apps depend on GDAL support. - if HAS_GDAL: - # Geographic admin, LayerMapping, and ogrinspect test apps - # all require GDAL. - apps.extend(['geoadmin', 'layermap', 'inspectapp']) - - # 3D apps use LayerMapping, which uses GDAL and require GEOS 3.1+. - if connection.ops.postgis and GEOS_PREPARE: - apps.append('geo3d') - return [('django.contrib.gis.tests', app) for app in apps] diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py index c34964a2da..df4158bb31 100644 --- a/django/contrib/gis/tests/geoadmin/tests.py +++ b/django/contrib/gis/tests/geoadmin/tests.py @@ -2,9 +2,10 @@ from __future__ import absolute_import from unittest import skipUnless -from django.test import TestCase from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB +from django.test import TestCase +from django.test.utils import override_settings if HAS_GEOS and HAS_SPATIAL_DB: from django.contrib.gis import admin @@ -12,6 +13,8 @@ if HAS_GEOS and HAS_SPATIAL_DB: from .models import City +GOOGLE_MAPS_API_KEY = 'XXXX' + @skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoAdminTest(TestCase): @@ -39,7 +42,9 @@ class GeoAdminTest(TestCase): result) def test_olwidget_has_changed(self): - """ Check that changes are accurately noticed by OpenLayersWidget. """ + """ + Check that changes are accurately noticed by OpenLayersWidget. + """ geoadmin = admin.site._registry[City] form = geoadmin.get_changelist_form(None)() has_changed = form.fields['point']._has_changed @@ -55,3 +60,15 @@ class GeoAdminTest(TestCase): self.assertFalse(has_changed(initial, data_same)) self.assertFalse(has_changed(initial, data_almost_same)) self.assertTrue(has_changed(initial, data_changed)) + + @override_settings(GOOGLE_MAPS_API_KEY=GOOGLE_MAPS_API_KEY) + def test_google_map_scripts(self): + """ + Testing GoogleMap.scripts() output. See #20773. + """ + from django.contrib.gis.maps.google.gmap import GoogleMap + + google_map = GoogleMap() + scripts = google_map.scripts + self.assertIn(GOOGLE_MAPS_API_KEY, scripts) + self.assertIn("new GMap2", scripts) diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py index facd94c247..98cd8cc5ac 100644 --- a/django/contrib/gis/tests/geoapp/test_sitemaps.py +++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from io import BytesIO from unittest import skipUnless from xml.dom import minidom +import os import zipfile from django.conf import settings @@ -10,6 +11,7 @@ from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB from django.contrib.sites.models import Site from django.test import TestCase +from django.utils._os import upath if HAS_GEOS: from .models import City, Country @@ -37,7 +39,11 @@ class GeoSitemapTest(TestCase): def test_geositemap_index(self): "Tests geographic sitemap index." # Getting the geo index. - doc = minidom.parseString(self.client.get('/sitemap.xml').content) + from django.contrib import sitemaps + template_dirs = settings.TEMPLATE_DIRS + ( + os.path.join(os.path.dirname(upath(sitemaps.__file__)), 'templates'),) + with self.settings(TEMPLATE_DIRS=template_dirs): + doc = minidom.parseString(self.client.get('/sitemap.xml').content) index = doc.firstChild self.assertEqual(index.getAttribute('xmlns'), 'http://www.sitemaps.org/schemas/sitemap/0.9') self.assertEqual(3, len(index.getElementsByTagName('sitemap'))) diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 487f879674..eabc5958c0 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -8,7 +8,7 @@ from django.db import connection from django.contrib.gis import gdal from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import ( - no_mysql, no_oracle, no_spatialite, + HAS_SPATIAL_DB, no_mysql, no_oracle, no_spatialite, mysql, oracle, postgis, spatialite) from django.test import TestCase from django.utils import six @@ -28,7 +28,7 @@ def postgis_bug_version(): return spatial_version and (2, 0, 0) <= spatial_version <= (2, 0, 1) -@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoModelTest(TestCase): def test_fixtures(self): @@ -203,7 +203,7 @@ class GeoModelTest(TestCase): self.assertTrue(isinstance(cities2[0].point, Point)) -@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoLookupTest(TestCase): @no_mysql @@ -389,7 +389,7 @@ class GeoLookupTest(TestCase): self.assertEqual('Lawrence', City.objects.get(point__relate=(ks.poly, intersects_mask)).name) -@skipUnless(HAS_GEOS and postgis, "Geos and postgis are required.") +@skipUnless(HAS_GEOS and HAS_SPATIAL_DB, "Geos and spatial db are required.") class GeoQuerySetTest(TestCase): # Please keep the tests in GeoQuerySet method's alphabetic order @@ -433,6 +433,13 @@ class GeoQuerySetTest(TestCase): self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference) self.assertEqual(c.mpoly.union(geom), c.union) + @skipUnless(getattr(connection.ops, 'envelope', False), 'Database does not support envelope operation') + def test_envelope(self): + "Testing the `envelope` GeoQuerySet method." + countries = Country.objects.all().envelope() + for country in countries: + self.assertIsInstance(country.envelope, Polygon) + @no_mysql @no_spatialite # SpatiaLite does not have an Extent function def test_extent(self): diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py index 8daa6acc23..5bb6a70206 100644 --- a/django/contrib/gis/tests/test_spatialrefsys.py +++ b/django/contrib/gis/tests/test_spatialrefsys.py @@ -23,7 +23,7 @@ test_srs = ({'srid' : 4326, 'auth_srid' : 32140, 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"', 'proj4_re' : r'\+proj=lcc \+lat_1=30.28333333333333 \+lat_2=28.38333333333333 \+lat_0=27.83333333333333 ' - r'\+lon_0=-99 \+x_0=600000 \+y_0=4000000 \+ellps=GRS80 ' + r'\+lon_0=-99 \+x_0=600000 \+y_0=4000000 (\+ellps=GRS80 )?' r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0)?\+units=m \+no_defs ', 'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central', 'geographic' : False, 'projected' : True, 'spatialite' : False, diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py index 08d3b40397..b7cfafd49d 100644 --- a/django/contrib/gis/utils/ogrinspect.py +++ b/django/contrib/gis/utils/ogrinspect.py @@ -89,7 +89,7 @@ def ogrinspect(*args, **kwargs): `multi_geom` => Boolean (default: False) - specify as multigeometry. `name_field` => String - specifies a field name to return for the - `__unicode__` function (which will be generated if specified). + `__unicode__`/`__str__` function (which will be generated if specified). `imports` => Boolean (default: True) - set to False to omit the `from django.contrib.gis.db import models` code from the @@ -221,4 +221,5 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non if name_field: yield '' - yield ' def __str__(self): return self.%s' % name_field + yield ' def __%s__(self): return self.%s' % ( + 'str' if six.PY3 else 'unicode', name_field) diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index 3bfba3ba08..cec204dc92 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -43,9 +43,9 @@ class Feed(object): raise Http404('Feed object does not exist.') feedgen = self.get_feed(obj, request) response = HttpResponse(content_type=feedgen.mime_type) - if hasattr(self, 'item_pubdate'): - # if item_pubdate is defined for the feed, set header so as - # ConditionalGetMiddleware is able to send 304 NOT MODIFIED + if hasattr(self, 'item_pubdate') or hasattr(self, 'item_updateddate'): + # if item_pubdate or item_updateddate is defined for the feed, set + # header so as ConditionalGetMiddleware is able to send 304 NOT MODIFIED response['Last-Modified'] = http_date( timegm(feedgen.latest_post_date().utctimetuple())) feedgen.write(response, 'utf-8') @@ -191,6 +191,11 @@ class Feed(object): ltz = tzinfo.LocalTimezone(pubdate) pubdate = pubdate.replace(tzinfo=ltz) + updateddate = self.__get_dynamic_attr('item_updateddate', item) + if updateddate and is_naive(updateddate): + ltz = tzinfo.LocalTimezone(updateddate) + updateddate = updateddate.replace(tzinfo=ltz) + feed.add_item( title = title, link = link, @@ -200,6 +205,7 @@ class Feed(object): 'item_guid_is_permalink', item), enclosure = enc, pubdate = pubdate, + updateddate = updateddate, author_name = author_name, author_email = author_email, author_link = author_link, diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py index 1fa17052fd..44d7db62c3 100644 --- a/django/core/cache/backends/locmem.py +++ b/django/core/cache/backends/locmem.py @@ -9,16 +9,17 @@ except ImportError: from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.synch import RWLock + # Global in-memory store of cache data. Keyed by name, to provide # multiple named local memory caches. _caches = {} _expire_info = {} _locks = {} + class LocMemCache(BaseCache): def __init__(self, name, params): BaseCache.__init__(self, params) - global _caches, _expire_info, _locks self._cache = _caches.setdefault(name, {}) self._expire_info = _expire_info.setdefault(name, {}) self._lock = _locks.setdefault(name, RWLock()) @@ -26,28 +27,31 @@ class LocMemCache(BaseCache): def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) + try: + pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) + except pickle.PickleError: + return False with self._lock.writer(): exp = self._expire_info.get(key) if exp is None or exp <= time.time(): - try: - pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) - self._set(key, pickled, timeout) - return True - except pickle.PickleError: - pass + self._set(key, pickled, timeout) + return True return False def get(self, key, default=None, version=None): key = self.make_key(key, version=version) self.validate_key(key) + pickled = None with self._lock.reader(): exp = self._expire_info.get(key, 0) if exp is None or exp > time.time(): - try: - pickled = self._cache[key] - return pickle.loads(pickled) - except pickle.PickleError: - return default + pickled = self._cache[key] + if pickled is not None: + try: + return pickle.loads(pickled) + except pickle.PickleError: + return default + with self._lock.writer(): try: del self._cache[key] @@ -68,12 +72,13 @@ class LocMemCache(BaseCache): def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) - with self._lock.writer(): - try: - pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) + try: + pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) + except pickle.PickleError: + pass + else: + with self._lock.writer(): self._set(key, pickled, timeout) - except pickle.PickleError: - pass def incr(self, key, delta=1, version=None): value = self.get(key, version=version) @@ -81,12 +86,13 @@ class LocMemCache(BaseCache): raise ValueError("Key '%s' not found" % key) new_value = value + delta key = self.make_key(key, version=version) - with self._lock.writer(): - try: - pickled = pickle.dumps(new_value, pickle.HIGHEST_PROTOCOL) + try: + pickled = pickle.dumps(new_value, pickle.HIGHEST_PROTOCOL) + except pickle.PickleError: + pass + else: + with self._lock.writer(): self._cache[key] = pickled - except pickle.PickleError: - pass return new_value def has_key(self, key, version=None): @@ -135,6 +141,7 @@ class LocMemCache(BaseCache): self._cache.clear() self._expire_info.clear() + # For backwards compatibility class CacheClass(LocMemCache): pass diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index 64d1c41dc5..19e8b02f74 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -86,7 +86,7 @@ class BaseMemcachedCache(BaseCache): self._cache.delete(key) def get_many(self, keys, version=None): - new_keys = map(lambda x: self.make_key(x, version=version), keys) + new_keys = [self.make_key(x, version=version) for x in keys] ret = self._cache.get_multi(new_keys) if ret: _ = {} diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py index 39b99ff78f..9f948ca03b 100644 --- a/django/core/files/uploadedfile.py +++ b/django/core/files/uploadedfile.py @@ -23,11 +23,12 @@ class UploadedFile(File): """ DEFAULT_CHUNK_SIZE = 64 * 2**10 - def __init__(self, file=None, name=None, content_type=None, size=None, charset=None): + def __init__(self, file=None, name=None, content_type=None, size=None, charset=None, content_type_extra=None): super(UploadedFile, self).__init__(file, name) self.size = size self.content_type = content_type self.charset = charset + self.content_type_extra = content_type_extra def __repr__(self): return force_str("<%s: %s (%s)>" % ( @@ -55,13 +56,13 @@ class TemporaryUploadedFile(UploadedFile): """ A file uploaded to a temporary location (i.e. stream-to-disk). """ - def __init__(self, name, content_type, size, charset): + def __init__(self, name, content_type, size, charset, content_type_extra): if settings.FILE_UPLOAD_TEMP_DIR: file = tempfile.NamedTemporaryFile(suffix='.upload', dir=settings.FILE_UPLOAD_TEMP_DIR) else: file = tempfile.NamedTemporaryFile(suffix='.upload') - super(TemporaryUploadedFile, self).__init__(file, name, content_type, size, charset) + super(TemporaryUploadedFile, self).__init__(file, name, content_type, size, charset, content_type_extra) def temporary_file_path(self): """ @@ -83,8 +84,8 @@ class InMemoryUploadedFile(UploadedFile): """ A file uploaded into memory (i.e. stream-to-memory). """ - def __init__(self, file, field_name, name, content_type, size, charset): - super(InMemoryUploadedFile, self).__init__(file, name, content_type, size, charset) + def __init__(self, file, field_name, name, content_type, size, charset, content_type_extra): + super(InMemoryUploadedFile, self).__init__(file, name, content_type, size, charset, content_type_extra) self.field_name = field_name def open(self, mode=None): @@ -109,7 +110,7 @@ class SimpleUploadedFile(InMemoryUploadedFile): def __init__(self, name, content, content_type='text/plain'): content = content or b'' super(SimpleUploadedFile, self).__init__(BytesIO(content), None, name, - content_type, len(content), None) + content_type, len(content), None, None) def from_dict(cls, file_dict): """ diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py index f5e95cf2fd..6739b26e0c 100644 --- a/django/core/files/uploadhandler.py +++ b/django/core/files/uploadhandler.py @@ -64,6 +64,7 @@ class FileUploadHandler(object): self.content_type = None self.content_length = None self.charset = None + self.content_type_extra = None self.request = request def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None): @@ -84,7 +85,7 @@ class FileUploadHandler(object): """ pass - def new_file(self, field_name, file_name, content_type, content_length, charset=None): + def new_file(self, field_name, file_name, content_type, content_length, charset=None, content_type_extra=None): """ Signal that a new file has been started. @@ -96,6 +97,7 @@ class FileUploadHandler(object): self.content_type = content_type self.content_length = content_length self.charset = charset + self.content_type_extra = content_type_extra def receive_data_chunk(self, raw_data, start): """ @@ -132,7 +134,7 @@ class TemporaryFileUploadHandler(FileUploadHandler): Create the file object to append to as data is coming in. """ super(TemporaryFileUploadHandler, self).new_file(file_name, *args, **kwargs) - self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset) + self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset, self.content_type_extra) def receive_data_chunk(self, raw_data, start): self.file.write(raw_data) @@ -187,7 +189,8 @@ class MemoryFileUploadHandler(FileUploadHandler): name = self.file_name, content_type = self.content_type, size = file_size, - charset = self.charset + charset = self.charset, + content_type_extra = self.content_type_extra ) diff --git a/django/core/mail/backends/console.py b/django/core/mail/backends/console.py index ea0cb5d9ad..389e06b37f 100644 --- a/django/core/mail/backends/console.py +++ b/django/core/mail/backends/console.py @@ -16,6 +16,7 @@ class EmailBackend(BaseEmailBackend): """Write all messages to the stream in a thread-safe way.""" if not email_messages: return + msg_count = 0 with self._lock: try: stream_created = self.open() @@ -24,9 +25,10 @@ class EmailBackend(BaseEmailBackend): self.stream.write('-' * 79) self.stream.write('\n') self.stream.flush() # flush after each message + msg_count += 1 if stream_created: self.close() except: if not self.fail_silently: raise - return len(email_messages) + return msg_count diff --git a/django/core/mail/backends/dummy.py b/django/core/mail/backends/dummy.py index 273aa0d88e..7ae4878f13 100644 --- a/django/core/mail/backends/dummy.py +++ b/django/core/mail/backends/dummy.py @@ -6,4 +6,4 @@ from django.core.mail.backends.base import BaseEmailBackend class EmailBackend(BaseEmailBackend): def send_messages(self, email_messages): - return len(email_messages) + return len(list(email_messages)) diff --git a/django/core/mail/backends/locmem.py b/django/core/mail/backends/locmem.py index 6826d09ee5..8e893af358 100644 --- a/django/core/mail/backends/locmem.py +++ b/django/core/mail/backends/locmem.py @@ -20,7 +20,9 @@ class EmailBackend(BaseEmailBackend): def send_messages(self, messages): """Redirect messages to the dummy outbox""" + msg_count = 0 for message in messages: # .message() triggers header validation message.message() + msg_count += 1 mail.outbox.extend(messages) - return len(messages) + return msg_count diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index e456b7864e..c7f4a1a801 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -15,22 +15,18 @@ class EmailBackend(BaseEmailBackend): A wrapper that manages the SMTP network connection. """ def __init__(self, host=None, port=None, username=None, password=None, - use_tls=None, fail_silently=False, **kwargs): + use_tls=None, fail_silently=False, use_ssl=None, **kwargs): super(EmailBackend, self).__init__(fail_silently=fail_silently) self.host = host or settings.EMAIL_HOST self.port = port or settings.EMAIL_PORT - if username is None: - self.username = settings.EMAIL_HOST_USER - else: - self.username = username - if password is None: - self.password = settings.EMAIL_HOST_PASSWORD - else: - self.password = password - if use_tls is None: - self.use_tls = settings.EMAIL_USE_TLS - else: - self.use_tls = use_tls + self.username = settings.EMAIL_HOST_USER if username is None else username + self.password = settings.EMAIL_HOST_PASSWORD if password is None else password + self.use_tls = settings.EMAIL_USE_TLS if use_tls is None else use_tls + self.use_ssl = settings.EMAIL_USE_SSL if use_ssl is None else use_ssl + if self.use_ssl and self.use_tls: + raise ValueError( + "EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set " + "one of those settings to True.") self.connection = None self._lock = threading.RLock() @@ -45,12 +41,18 @@ class EmailBackend(BaseEmailBackend): try: # If local_hostname is not specified, socket.getfqdn() gets used. # For performance, we use the cached FQDN for local_hostname. - self.connection = smtplib.SMTP(self.host, self.port, + if self.use_ssl: + self.connection = smtplib.SMTP_SSL(self.host, self.port, local_hostname=DNS_NAME.get_fqdn()) - if self.use_tls: - self.connection.ehlo() - self.connection.starttls() - self.connection.ehlo() + else: + self.connection = smtplib.SMTP(self.host, self.port, + local_hostname=DNS_NAME.get_fqdn()) + # TLS/SSL are mutually exclusive, so only attempt TLS over + # non-secure connections. + if self.use_tls: + self.connection.ehlo() + self.connection.starttls() + self.connection.ehlo() if self.username and self.password: self.connection.login(self.username, self.password) return True diff --git a/django/core/mail/message.py b/django/core/mail/message.py index 98ab3c9075..db9023a0bb 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import mimetypes import os import random -import sys import time from email import charset as Charset, encoders as Encoders from email.generator import Generator @@ -139,9 +138,6 @@ class SafeMIMEText(MIMEText): """ fp = six.StringIO() g = Generator(fp, mangle_from_ = False) - if sys.version_info < (2, 6, 6) and isinstance(self._payload, six.text_type): - # Workaround for http://bugs.python.org/issue1368247 - self._payload = self._payload.encode(self._charset.output_charset) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() diff --git a/django/core/signing.py b/django/core/signing.py index bbe53aafe3..c3b2c3ed36 100644 --- a/django/core/signing.py +++ b/django/core/signing.py @@ -183,6 +183,10 @@ class TimestampSigner(Signer): return super(TimestampSigner, self).sign(value) def unsign(self, value, max_age=None): + """ + Retrieve original value and check it wasn't signed more + than max_age seconds ago. + """ result = super(TimestampSigner, self).unsign(value) value, timestamp = result.rsplit(self.sep, 1) timestamp = baseconv.base62.decode(timestamp) diff --git a/django/core/validators.py b/django/core/validators.py index 200d28fe02..aa417ed099 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -87,7 +87,7 @@ class EmailValidator(object): r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"$)', # quoted-string re.IGNORECASE) domain_regex = re.compile( - r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)' # domain + r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\.?$' # domain # literal form, ipv4 address (SMTP 4.1.3) r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) diff --git a/django/db/__init__.py b/django/db/__init__.py index 0d7fbe2a34..e1af430dc2 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -1,20 +1,25 @@ import warnings from django.core import signals -from django.db.utils import (DEFAULT_DB_ALIAS, - DataError, OperationalError, IntegrityError, InternalError, - ProgrammingError, NotSupportedError, DatabaseError, - InterfaceError, Error, - load_backend, ConnectionHandler, ConnectionRouter) +from django.db.utils import (DEFAULT_DB_ALIAS, DataError, OperationalError, + IntegrityError, InternalError, ProgrammingError, NotSupportedError, + DatabaseError, InterfaceError, Error, load_backend, + ConnectionHandler, ConnectionRouter) from django.utils.functional import cached_property -__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError', - 'IntegrityError', 'DEFAULT_DB_ALIAS') + +__all__ = [ + 'backend', 'connection', 'connections', 'router', 'DatabaseError', + 'IntegrityError', 'InternalError', 'ProgrammingError', 'DataError', + 'NotSupportedError', 'Error', 'InterfaceError', 'OperationalError', + 'DEFAULT_DB_ALIAS' +] connections = ConnectionHandler() router = ConnectionRouter() + # `connection`, `DatabaseError` and `IntegrityError` are convenient aliases # for backend bits. @@ -39,8 +44,15 @@ class DefaultConnectionProxy(object): def __delattr__(self, name): return delattr(connections[DEFAULT_DB_ALIAS], name) + def __eq__(self, other): + return connections[DEFAULT_DB_ALIAS] == other + + def __ne__(self, other): + return connections[DEFAULT_DB_ALIAS] != other + connection = DefaultConnectionProxy() + class DefaultBackendProxy(object): """ Temporary proxy class used during deprecation period of the `backend` module @@ -63,6 +75,7 @@ class DefaultBackendProxy(object): backend = DefaultBackendProxy() + def close_connection(**kwargs): warnings.warn( "close_connection is superseded by close_old_connections.", @@ -76,12 +89,14 @@ def close_connection(**kwargs): transaction.abort(conn) connections[conn].close() + # Register an event to reset saved queries when a Django request is started. def reset_queries(**kwargs): for conn in connections.all(): conn.queries = [] signals.request_started.connect(reset_queries) + # Register an event to reset transaction state and close connections past # their lifetime. NB: abort() doesn't do anything outside of a transaction. def close_old_connections(**kwargs): diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 451cf4baf1..80e66b3ad4 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -70,7 +70,9 @@ class BaseDatabaseWrapper(object): self._thread_ident = thread.get_ident() def __eq__(self, other): - return self.alias == other.alias + if isinstance(other, BaseDatabaseWrapper): + return self.alias == other.alias + return NotImplemented def __ne__(self, other): return not self == other @@ -1184,6 +1186,7 @@ FieldInfo = namedtuple('FieldInfo', 'name type_code display_size internal_size precision scale null_ok' ) + class BaseDatabaseIntrospection(object): """ This class encapsulates all backend-specific introspection utilities diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 2767fcb48d..2ebbbbf2d5 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -253,12 +253,13 @@ class BaseDatabaseCreation(object): r_col = model._meta.get_field(f.rel.field_name).column r_name = '%s_refs_%s_%s' % ( col, r_col, self._digest(table, r_table)) - output.append('%s %s %s %s;' % \ - (style.SQL_KEYWORD('ALTER TABLE'), + output.append('%s %s %s %s;' % ( + style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(table)), style.SQL_KEYWORD(self.connection.ops.drop_foreignkey_sql()), style.SQL_FIELD(qn(truncate_name( - r_name, self.connection.ops.max_name_length()))))) + r_name, self.connection.ops.max_name_length()))) + )) del references_to_delete[model] return output diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 9a220ffd8b..3e6d3e4c5a 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -8,33 +8,43 @@ ImproperlyConfigured. """ from django.core.exceptions import ImproperlyConfigured -from django.db.backends import * +from django.db.backends import (BaseDatabaseOperations, BaseDatabaseClient, + BaseDatabaseIntrospection, BaseDatabaseWrapper, BaseDatabaseFeatures, + BaseDatabaseValidation) from django.db.backends.creation import BaseDatabaseCreation + def complain(*args, **kwargs): raise ImproperlyConfigured("settings.DATABASES is improperly configured. " "Please supply the ENGINE value. Check " "settings documentation for more details.") + def ignore(*args, **kwargs): pass + class DatabaseError(Exception): pass + class IntegrityError(DatabaseError): pass + class DatabaseOperations(BaseDatabaseOperations): quote_name = complain + class DatabaseClient(BaseDatabaseClient): runshell = complain + class DatabaseCreation(BaseDatabaseCreation): create_test_db = ignore destroy_test_db = ignore + class DatabaseIntrospection(BaseDatabaseIntrospection): get_table_list = complain get_table_description = complain @@ -42,6 +52,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): get_indexes = complain get_key_columns = complain + class DatabaseWrapper(BaseDatabaseWrapper): operators = {} # Override the base class implementations with null diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 8a689f66ed..15eacec679 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -37,7 +37,8 @@ except ImportError: from django.conf import settings from django.db import utils -from django.db.backends import * +from django.db.backends import (util, BaseDatabaseFeatures, + BaseDatabaseOperations, BaseDatabaseWrapper) from django.db.backends.mysql.client import DatabaseClient from django.db.backends.mysql.creation import DatabaseCreation from django.db.backends.mysql.introspection import DatabaseIntrospection @@ -60,6 +61,7 @@ IntegrityError = Database.IntegrityError # It's impossible to import datetime_or_None directly from MySQLdb.times parse_datetime = conversions[FIELD_TYPE.DATETIME] + def parse_datetime_with_timezone_support(value): dt = parse_datetime(value) # Confirm that dt is naive before overwriting its tzinfo. @@ -67,6 +69,7 @@ def parse_datetime_with_timezone_support(value): dt = dt.replace(tzinfo=timezone.utc) return dt + def adapt_datetime_with_timezone_support(value, conv): # Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL. if settings.USE_TZ: @@ -101,6 +104,7 @@ django_conversions.update({ # http://dev.mysql.com/doc/refman/5.0/en/news.html . server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') + # MySQLdb-1.2.1 and newer automatically makes use of SHOW WARNINGS on # MySQL-4.1 and newer, so the MysqlDebugWrapper is unnecessary. Since the # point is to raise Warnings as exceptions, this can be done with the Python @@ -151,6 +155,7 @@ class CursorWrapper(object): def __iter__(self): return iter(self.cursor) + class DatabaseFeatures(BaseDatabaseFeatures): empty_fetchmany_value = () update_can_self_select = False @@ -208,6 +213,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1") return cursor.fetchone() is not None + class DatabaseOperations(BaseDatabaseOperations): compiler_module = "django.db.backends.mysql.compiler" @@ -323,7 +329,7 @@ class DatabaseOperations(BaseDatabaseOperations): # Truncate already resets the AUTO_INCREMENT field from # MySQL version 5.0.13 onwards. Refs #16961. if self.connection.mysql_version < (5, 0, 13): - return ["%s %s %s %s %s;" % \ + return ["%s %s %s %s %s;" % (style.SQL_KEYWORD('ALTER'), style.SQL_KEYWORD('TABLE'), style.SQL_TABLE(self.quote_name(sequence['table'])), @@ -377,6 +383,7 @@ class DatabaseOperations(BaseDatabaseOperations): items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) return "VALUES " + ", ".join([items_sql] * num_values) + class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'mysql' operators = { diff --git a/django/db/backends/mysql/client.py b/django/db/backends/mysql/client.py index 1cf8ceef9c..8364c7b6e6 100644 --- a/django/db/backends/mysql/client.py +++ b/django/db/backends/mysql/client.py @@ -3,6 +3,7 @@ import sys from django.db.backends import BaseDatabaseClient + class DatabaseClient(BaseDatabaseClient): executable_name = 'mysql' @@ -37,4 +38,3 @@ class DatabaseClient(BaseDatabaseClient): sys.exit(os.system(" ".join(args))) else: os.execvp(self.executable_name, args) - diff --git a/django/db/backends/mysql/compiler.py b/django/db/backends/mysql/compiler.py index 4e033e3d93..b7d1d7b98d 100644 --- a/django/db/backends/mysql/compiler.py +++ b/django/db/backends/mysql/compiler.py @@ -22,20 +22,26 @@ class SQLCompiler(compiler.SQLCompiler): sql, params = self.as_sql() return '(%s) IN (%s)' % (', '.join(['%s.%s' % (qn(alias), qn2(column)) for column in columns]), sql), params + class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): pass + class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): pass + class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): pass + class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): pass + class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): pass + class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler): pass diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index 3a57c29479..b59b225f4f 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -1,34 +1,35 @@ from django.db.backends.creation import BaseDatabaseCreation + class DatabaseCreation(BaseDatabaseCreation): # This dictionary maps Field objects to their associated MySQL column # types, as strings. Column-type strings can contain format strings; they'll # be interpolated against the values of Field.__dict__ before being output. # If a column type is set to None, it won't be included in the output. data_types = { - 'AutoField': 'integer AUTO_INCREMENT', - 'BinaryField': 'longblob', - 'BooleanField': 'bool', - 'CharField': 'varchar(%(max_length)s)', + 'AutoField': 'integer AUTO_INCREMENT', + 'BinaryField': 'longblob', + 'BooleanField': 'bool', + 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', - 'DateField': 'date', - 'DateTimeField': 'datetime', - 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'varchar(%(max_length)s)', - 'FilePathField': 'varchar(%(max_length)s)', - 'FloatField': 'double precision', - 'IntegerField': 'integer', - 'BigIntegerField': 'bigint', - 'IPAddressField': 'char(15)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'double precision', + 'IntegerField': 'integer', + 'BigIntegerField': 'bigint', + 'IPAddressField': 'char(15)', 'GenericIPAddressField': 'char(39)', - 'NullBooleanField': 'bool', - 'OneToOneField': 'integer', + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', 'PositiveIntegerField': 'integer UNSIGNED', 'PositiveSmallIntegerField': 'smallint UNSIGNED', - 'SlugField': 'varchar(%(max_length)s)', + 'SlugField': 'varchar(%(max_length)s)', 'SmallIntegerField': 'smallint', - 'TextField': 'longtext', - 'TimeField': 'time', + 'TextField': 'longtext', + 'TimeField': 'time', } def sql_table_creation_suffix(self): diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index 00c567a90b..e2d382b895 100644 --- a/django/db/backends/mysql/introspection.py +++ b/django/db/backends/mysql/introspection.py @@ -7,6 +7,7 @@ from django.utils.encoding import force_text foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") + class DatabaseIntrospection(BaseDatabaseIntrospection): data_types_reverse = { FIELD_TYPE.BLOB: 'TextField', diff --git a/django/db/backends/mysql/validation.py b/django/db/backends/mysql/validation.py index 2ce957cce7..17b7cde756 100644 --- a/django/db/backends/mysql/validation.py +++ b/django/db/backends/mysql/validation.py @@ -1,5 +1,6 @@ from django.db.backends import BaseDatabaseValidation + class DatabaseValidation(BaseDatabaseValidation): def validate_field(self, errors, opts, f): """ diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 5fb806807d..63d38bdafd 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -7,11 +7,12 @@ from __future__ import unicode_literals import decimal import re +import platform import sys import warnings + def _setup_environment(environ): - import platform # Cygwin requires some special voodoo to set the environment variables # properly so that Oracle will see them. if platform.system().upper().startswith('CYGWIN'): @@ -91,6 +92,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_tablespaces = True supports_sequence_reset = False + class DatabaseOperations(BaseDatabaseOperations): compiler_module = "django.db.backends.oracle.compiler" @@ -268,7 +270,7 @@ WHEN (new.%(col_name)s IS NULL) # http://cx-oracle.sourceforge.net/html/cursor.html#Cursor.statement # The DB API definition does not define this attribute. statement = cursor.statement - if not six.PY3 and not isinstance(statement, unicode): + if statement and not six.PY3 and not isinstance(statement, unicode): statement = statement.decode('utf-8') # Unlike Psycopg's `query` and MySQLdb`'s `_last_executed`, CxOracle's # `statement` doesn't contain the query parameters. refs #20010. @@ -309,7 +311,7 @@ WHEN (new.%(col_name)s IS NULL) # Oracle puts the query text into a (query % args) construct, so % signs # in names need to be escaped. The '%%' will be collapsed back to '%' at # that stage so we aren't really making the name longer here. - name = name.replace('%','%%') + name = name.replace('%', '%%') return name.upper() def random_function_sql(self): @@ -763,7 +765,7 @@ class FormatStylePlaceholderCursor(object): def _format_params(self, params): try: - return dict((k,OracleParam(v, self, True)) for k,v in params.items()) + return dict((k, OracleParam(v, self, True)) for k, v in params.items()) except AttributeError: return tuple([OracleParam(p, self, True) for p in params]) @@ -783,12 +785,12 @@ class FormatStylePlaceholderCursor(object): for i, value in enumerate(params): if value.input_size: sizes[i] = value.input_size - self.setinputsizes(*sizes) + self.setinputsizes(*sizes) def _param_generator(self, params): # Try dict handling; if that fails, treat as sequence if hasattr(params, 'items'): - return dict((k, v.force_bytes) for k,v in params.items()) + return dict((k, v.force_bytes) for k, v in params.items()) else: return [p.force_bytes for p in params] @@ -804,14 +806,14 @@ class FormatStylePlaceholderCursor(object): query = convert_unicode(query, self.charset) elif hasattr(params, 'keys'): # Handle params as dict - args = dict((k, ":%s"%k) for k in params.keys()) + args = dict((k, ":%s" % k) for k in params.keys()) query = convert_unicode(query % args, self.charset) else: # Handle params as sequence args = [(':arg%d' % i) for i in range(len(params))] query = convert_unicode(query % tuple(args), self.charset) return query, self._format_params(params) - + def execute(self, query, params=None): query, params = self._fix_for_params(query, params) self._guess_input_sizes([params]) @@ -830,9 +832,9 @@ class FormatStylePlaceholderCursor(object): # uniform treatment for sequences and iterables params_iter = iter(params) query, firstparams = self._fix_for_params(query, next(params_iter)) - # we build a list of formatted params; as we're going to traverse it + # we build a list of formatted params; as we're going to traverse it # more than once, we can't make it lazy by using a generator - formatted = [firstparams]+[self._format_params(p) for p in params_iter] + formatted = [firstparams] + [self._format_params(p) for p in params_iter] self._guess_input_sizes(formatted) try: return self.cursor.executemany(query, diff --git a/django/db/backends/oracle/client.py b/django/db/backends/oracle/client.py index ccc64ebffc..ac6b79041f 100644 --- a/django/db/backends/oracle/client.py +++ b/django/db/backends/oracle/client.py @@ -3,6 +3,7 @@ import sys from django.db.backends import BaseDatabaseClient + class DatabaseClient(BaseDatabaseClient): executable_name = 'sqlplus' @@ -13,4 +14,3 @@ class DatabaseClient(BaseDatabaseClient): sys.exit(os.system(" ".join(args))) else: os.execvp(self.executable_name, args) - diff --git a/django/db/backends/oracle/compiler.py b/django/db/backends/oracle/compiler.py index cbee27951c..d2d4cd3ac9 100644 --- a/django/db/backends/oracle/compiler.py +++ b/django/db/backends/oracle/compiler.py @@ -60,17 +60,22 @@ class SQLCompiler(compiler.SQLCompiler): class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): pass + class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): pass + class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): pass + class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler): pass + class SQLDateCompiler(compiler.SQLDateCompiler, SQLCompiler): pass + class SQLDateTimeCompiler(compiler.SQLDateTimeCompiler, SQLCompiler): pass diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 7f1192ed8a..f7d43ae7b6 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -5,9 +5,11 @@ from django.conf import settings from django.db.backends.creation import BaseDatabaseCreation from django.utils.six.moves import input + TEST_DATABASE_PREFIX = 'test_' PASSWORD = 'Im_a_lumberjack' + class DatabaseCreation(BaseDatabaseCreation): # This dictionary maps Field objects to their associated Oracle column # types, as strings. Column-type strings can contain format strings; they'll @@ -18,30 +20,30 @@ class DatabaseCreation(BaseDatabaseCreation): # output (the "qn_" prefix is stripped before the lookup is performed. data_types = { - 'AutoField': 'NUMBER(11)', - 'BinaryField': 'BLOB', - 'BooleanField': 'NUMBER(1)', - 'CharField': 'NVARCHAR2(%(max_length)s)', - 'CommaSeparatedIntegerField': 'VARCHAR2(%(max_length)s)', - 'DateField': 'DATE', - 'DateTimeField': 'TIMESTAMP', - 'DecimalField': 'NUMBER(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'NVARCHAR2(%(max_length)s)', - 'FilePathField': 'NVARCHAR2(%(max_length)s)', - 'FloatField': 'DOUBLE PRECISION', - 'IntegerField': 'NUMBER(11)', - 'BigIntegerField': 'NUMBER(19)', - 'IPAddressField': 'VARCHAR2(15)', - 'GenericIPAddressField': 'VARCHAR2(39)', - 'NullBooleanField': 'NUMBER(1)', - 'OneToOneField': 'NUMBER(11)', - 'PositiveIntegerField': 'NUMBER(11)', - 'PositiveSmallIntegerField': 'NUMBER(11)', - 'SlugField': 'NVARCHAR2(%(max_length)s)', - 'SmallIntegerField': 'NUMBER(11)', - 'TextField': 'NCLOB', - 'TimeField': 'TIMESTAMP', - 'URLField': 'VARCHAR2(%(max_length)s)', + 'AutoField': 'NUMBER(11)', + 'BinaryField': 'BLOB', + 'BooleanField': 'NUMBER(1)', + 'CharField': 'NVARCHAR2(%(max_length)s)', + 'CommaSeparatedIntegerField': 'VARCHAR2(%(max_length)s)', + 'DateField': 'DATE', + 'DateTimeField': 'TIMESTAMP', + 'DecimalField': 'NUMBER(%(max_digits)s, %(decimal_places)s)', + 'FileField': 'NVARCHAR2(%(max_length)s)', + 'FilePathField': 'NVARCHAR2(%(max_length)s)', + 'FloatField': 'DOUBLE PRECISION', + 'IntegerField': 'NUMBER(11)', + 'BigIntegerField': 'NUMBER(19)', + 'IPAddressField': 'VARCHAR2(15)', + 'GenericIPAddressField': 'VARCHAR2(39)', + 'NullBooleanField': 'NUMBER(1)', + 'OneToOneField': 'NUMBER(11)', + 'PositiveIntegerField': 'NUMBER(11)', + 'PositiveSmallIntegerField': 'NUMBER(11)', + 'SlugField': 'NVARCHAR2(%(max_length)s)', + 'SmallIntegerField': 'NUMBER(11)', + 'TextField': 'NCLOB', + 'TimeField': 'TIMESTAMP', + 'URLField': 'VARCHAR2(%(max_length)s)', } data_type_check_constraints = { @@ -190,7 +192,7 @@ class DatabaseCreation(BaseDatabaseCreation): statements = [ 'DROP TABLESPACE %(tblspace)s INCLUDING CONTENTS AND DATAFILES CASCADE CONSTRAINTS', 'DROP TABLESPACE %(tblspace_temp)s INCLUDING CONTENTS AND DATAFILES CASCADE CONSTRAINTS', - ] + ] self._execute_statements(cursor, statements, parameters, verbosity) def _destroy_test_user(self, cursor, parameters, verbosity): diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index 361308a62c..a2fad92509 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -1,10 +1,13 @@ +import re + +import cx_Oracle + from django.db.backends import BaseDatabaseIntrospection, FieldInfo from django.utils.encoding import force_text -import cx_Oracle -import re foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") + class DatabaseIntrospection(BaseDatabaseIntrospection): # Maps type objects to Django Field types. data_types_reverse = { @@ -95,11 +98,11 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): SELECT ccol.column_name, rcol.table_name AS referenced_table, rcol.column_name AS referenced_column FROM user_constraints c JOIN user_cons_columns ccol - ON ccol.constraint_name = c.constraint_name + ON ccol.constraint_name = c.constraint_name JOIN user_cons_columns rcol - ON rcol.constraint_name = c.r_constraint_name - WHERE c.table_name = %s AND c.constraint_type = 'R'""" , [table_name.upper()]) - return [tuple(cell.lower() for cell in row) + ON rcol.constraint_name = c.r_constraint_name + WHERE c.table_name = %s AND c.constraint_type = 'R'""", [table_name.upper()]) + return [tuple(cell.lower() for cell in row) for row in cursor.fetchall()] def get_indexes(self, cursor, table_name): diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index bf75459b4a..f283476429 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -6,7 +6,9 @@ Requires psycopg 2: http://initd.org/projects/psycopg2 import logging import sys -from django.db.backends import * +from django.conf import settings +from django.db.backends import (BaseDatabaseFeatures, BaseDatabaseWrapper, + BaseDatabaseValidation) from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations from django.db.backends.postgresql_psycopg2.client import DatabaseClient from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation @@ -34,11 +36,13 @@ psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) logger = logging.getLogger('django.db.backends') + def utc_tzinfo_factory(offset): if offset != 0: raise AssertionError("database connection isn't set to UTC") return utc + class DatabaseFeatures(BaseDatabaseFeatures): needs_datetime_string_cast = False can_return_id_from_insert = True @@ -55,6 +59,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): can_rollback_ddl = True supports_combined_alters = True + class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'postgresql' operators = { @@ -135,7 +140,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): # Set the time zone in autocommit mode (see #17062) self.set_autocommit(True) self.connection.cursor().execute( - self.ops.set_time_zone_sql(), [tz]) + self.ops.set_time_zone_sql(), [tz] + ) self.connection.set_isolation_level(self.isolation_level) def create_cursor(self): diff --git a/django/db/backends/postgresql_psycopg2/client.py b/django/db/backends/postgresql_psycopg2/client.py index a5c02969ea..23ac9f2975 100644 --- a/django/db/backends/postgresql_psycopg2/client.py +++ b/django/db/backends/postgresql_psycopg2/client.py @@ -3,6 +3,7 @@ import sys from django.db.backends import BaseDatabaseClient + class DatabaseClient(BaseDatabaseClient): executable_name = 'psql' @@ -20,4 +21,3 @@ class DatabaseClient(BaseDatabaseClient): sys.exit(os.system(" ".join(args))) else: os.execvp(self.executable_name, args) - diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index 1bfda6354d..954e198764 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -8,29 +8,29 @@ class DatabaseCreation(BaseDatabaseCreation): # be interpolated against the values of Field.__dict__ before being output. # If a column type is set to None, it won't be included in the output. data_types = { - 'AutoField': 'serial', - 'BinaryField': 'bytea', - 'BooleanField': 'boolean', - 'CharField': 'varchar(%(max_length)s)', + 'AutoField': 'serial', + 'BinaryField': 'bytea', + 'BooleanField': 'boolean', + 'CharField': 'varchar(%(max_length)s)', 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', - 'DateField': 'date', - 'DateTimeField': 'timestamp with time zone', - 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'FileField': 'varchar(%(max_length)s)', - 'FilePathField': 'varchar(%(max_length)s)', - 'FloatField': 'double precision', - 'IntegerField': 'integer', - 'BigIntegerField': 'bigint', - 'IPAddressField': 'inet', + 'DateField': 'date', + 'DateTimeField': 'timestamp with time zone', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'double precision', + 'IntegerField': 'integer', + 'BigIntegerField': 'bigint', + 'IPAddressField': 'inet', 'GenericIPAddressField': 'inet', - 'NullBooleanField': 'boolean', - 'OneToOneField': 'integer', + 'NullBooleanField': 'boolean', + 'OneToOneField': 'integer', 'PositiveIntegerField': 'integer', 'PositiveSmallIntegerField': 'smallint', - 'SlugField': 'varchar(%(max_length)s)', + 'SlugField': 'varchar(%(max_length)s)', 'SmallIntegerField': 'smallint', - 'TextField': 'text', - 'TimeField': 'time', + 'TextField': 'text', + 'TimeField': 'time', } data_type_check_constraints = { @@ -59,7 +59,7 @@ class DatabaseCreation(BaseDatabaseCreation): def get_index_sql(index_name, opclass=''): return (style.SQL_KEYWORD('CREATE INDEX') + ' ' + - style.SQL_TABLE(qn(truncate_name(index_name,self.connection.ops.max_name_length()))) + ' ' + + style.SQL_TABLE(qn(truncate_name(index_name, self.connection.ops.max_name_length()))) + ' ' + style.SQL_KEYWORD('ON') + ' ' + style.SQL_TABLE(qn(db_table)) + ' ' + "(%s%s)" % (style.SQL_FIELD(qn(f.column)), opclass) + diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py index 4f77059376..a816bb34d8 100644 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ b/django/db/backends/postgresql_psycopg2/introspection.py @@ -25,7 +25,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): 1266: 'TimeField', 1700: 'DecimalField', } - + def get_table_list(self, cursor): "Returns a list of table names in the current database." cursor.execute(""" @@ -47,7 +47,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): WHERE table_name = %s""", [table_name]) null_map = dict(cursor.fetchall()) cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return [FieldInfo(*((force_text(line[0]),) + line[1:6] + (null_map[force_text(line[0])]=='YES',))) + return [FieldInfo(*((force_text(line[0]),) + line[1:6] + (null_map[force_text(line[0])] == 'YES',))) for line in cursor.description] def get_relations(self, cursor, table_name): @@ -81,7 +81,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): ON ccu.constraint_catalog = tc.constraint_catalog AND ccu.constraint_schema = tc.constraint_schema AND ccu.constraint_name = tc.constraint_name - WHERE kcu.table_name = %s AND tc.constraint_type = 'FOREIGN KEY'""" , [table_name]) + WHERE kcu.table_name = %s AND tc.constraint_type = 'FOREIGN KEY'""", [table_name]) key_columns.extend(cursor.fetchall()) return key_columns diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index c5aab84693..cc78ffe449 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -135,7 +135,7 @@ class DatabaseOperations(BaseDatabaseOperations): # This will be the case if it's an m2m using an autogenerated # intermediate table (see BaseDatabaseIntrospection.sequence_list) column_name = 'id' - sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % \ + sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % (style.SQL_KEYWORD('SELECT'), style.SQL_TABLE(self.quote_name(table_name)), style.SQL_FIELD(column_name)) @@ -161,7 +161,7 @@ class DatabaseOperations(BaseDatabaseOperations): for f in model._meta.local_fields: if isinstance(f, models.AutoField): - output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \ + output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % (style.SQL_KEYWORD('SELECT'), style.SQL_TABLE(qn(model._meta.db_table)), style.SQL_FIELD(f.column), @@ -173,7 +173,7 @@ class DatabaseOperations(BaseDatabaseOperations): break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.many_to_many: if not f.rel.through: - output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \ + output.append("%s setval(pg_get_serial_sequence('%s','%s'), coalesce(max(%s), 1), max(%s) %s null) %s %s;" % (style.SQL_KEYWORD('SELECT'), style.SQL_TABLE(qn(f.m2m_db_table())), style.SQL_FIELD('id'), diff --git a/django/db/backends/postgresql_psycopg2/version.py b/django/db/backends/postgresql_psycopg2/version.py index 8ef516704e..dae94f2dac 100644 --- a/django/db/backends/postgresql_psycopg2/version.py +++ b/django/db/backends/postgresql_psycopg2/version.py @@ -19,7 +19,8 @@ def _parse_version(text): try: return int(major) * 10000 + int(major2) * 100 + int(minor) except (ValueError, TypeError): - return int(major) * 10000 + int(major2) * 100 + return int(major) * 10000 + int(major2) * 100 + def get_version(connection): """ diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index e330082108..960b6def03 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -11,8 +11,10 @@ import decimal import warnings import re +from django.conf import settings from django.db import utils -from django.db.backends import * +from django.db.backends import (util, BaseDatabaseFeatures, + BaseDatabaseOperations, BaseDatabaseWrapper, BaseDatabaseValidation) from django.db.backends.sqlite3.client import DatabaseClient from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.introspection import DatabaseIntrospection @@ -43,6 +45,7 @@ except ImportError: DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError + def parse_datetime_with_timezone_support(value): dt = parse_datetime(value) # Confirm that dt is naive before overwriting its tzinfo. @@ -50,6 +53,7 @@ def parse_datetime_with_timezone_support(value): dt = dt.replace(tzinfo=timezone.utc) return dt + def adapt_datetime_with_timezone_support(value): # Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL. if settings.USE_TZ: @@ -62,6 +66,7 @@ def adapt_datetime_with_timezone_support(value): value = value.astimezone(timezone.utc).replace(tzinfo=None) return value.isoformat(str(" ")) + def decoder(conv_func): """ The Python sqlite3 interface returns always byte strings. This function converts the received value to a regular string before @@ -82,6 +87,7 @@ Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal) Database.register_adapter(str, lambda s: s.decode('utf-8')) Database.register_adapter(SafeBytes, lambda s: s.decode('utf-8')) + class DatabaseFeatures(BaseDatabaseFeatures): # SQLite cannot handle us only partially reading from a cursor's result set # and then writing the same rows to the database in another cursor. This @@ -127,6 +133,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): def has_zoneinfo_database(self): return pytz is not None + class DatabaseOperations(BaseDatabaseOperations): def bulk_batch_size(self, fields, objs): """ @@ -275,6 +282,7 @@ class DatabaseOperations(BaseDatabaseOperations): res.extend(["UNION ALL SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1)) return " ".join(res) + class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'sqlite' # SQLite requires LIKE statements to include an ESCAPE clause if the value @@ -433,6 +441,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): FORMAT_QMARK_REGEX = re.compile(r'(?<!%)%s') + class SQLiteCursorWrapper(Database.Cursor): """ Django uses "format" style placeholders, but pysqlite2 uses "qmark" style. @@ -452,6 +461,7 @@ class SQLiteCursorWrapper(Database.Cursor): def convert_query(self, query): return FORMAT_QMARK_REGEX.sub('?', query).replace('%%', '%') + def _sqlite_date_extract(lookup_type, dt): if dt is None: return None @@ -464,6 +474,7 @@ def _sqlite_date_extract(lookup_type, dt): else: return getattr(dt, lookup_type) + def _sqlite_date_trunc(lookup_type, dt): try: dt = util.typecast_timestamp(dt) @@ -476,6 +487,7 @@ def _sqlite_date_trunc(lookup_type, dt): elif lookup_type == 'day': return "%i-%02i-%02i" % (dt.year, dt.month, dt.day) + def _sqlite_datetime_extract(lookup_type, dt, tzname): if dt is None: return None @@ -490,6 +502,7 @@ def _sqlite_datetime_extract(lookup_type, dt, tzname): else: return getattr(dt, lookup_type) + def _sqlite_datetime_trunc(lookup_type, dt, tzname): try: dt = util.typecast_timestamp(dt) @@ -510,6 +523,7 @@ def _sqlite_datetime_trunc(lookup_type, dt, tzname): elif lookup_type == 'second': return "%i-%02i-%02i %02i:%02i:%02i" % (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) + def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): try: dt = util.typecast_timestamp(dt) @@ -524,5 +538,6 @@ def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): # It will be formatted as "%Y-%m-%d" or "%Y-%m-%d %H:%M:%S[.%f]" return str(dt) + def _sqlite_regexp(re_pattern, re_string): return bool(re.search(re_pattern, force_text(re_string))) if re_string is not None else False diff --git a/django/db/backends/sqlite3/client.py b/django/db/backends/sqlite3/client.py index 5b5b7326f2..6a3ad9e76f 100644 --- a/django/db/backends/sqlite3/client.py +++ b/django/db/backends/sqlite3/client.py @@ -3,6 +3,7 @@ import sys from django.db.backends import BaseDatabaseClient + class DatabaseClient(BaseDatabaseClient): executable_name = 'sqlite3' @@ -13,4 +14,3 @@ class DatabaseClient(BaseDatabaseClient): sys.exit(os.system(" ".join(args))) else: os.execvp(self.executable_name, args) - diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index a9fb273f7a..435cee3436 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -1,36 +1,38 @@ import os import sys + from django.db.backends.creation import BaseDatabaseCreation from django.utils.six.moves import input + class DatabaseCreation(BaseDatabaseCreation): # SQLite doesn't actually support most of these types, but it "does the right # thing" given more verbose field definitions, so leave them as is so that # schema inspection is more useful. data_types = { - 'AutoField': 'integer', - 'BinaryField': 'BLOB', - 'BooleanField': 'bool', - 'CharField': 'varchar(%(max_length)s)', - 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', - 'DateField': 'date', - 'DateTimeField': 'datetime', - 'DecimalField': 'decimal', - 'FileField': 'varchar(%(max_length)s)', - 'FilePathField': 'varchar(%(max_length)s)', - 'FloatField': 'real', - 'IntegerField': 'integer', - 'BigIntegerField': 'bigint', - 'IPAddressField': 'char(15)', - 'GenericIPAddressField': 'char(39)', - 'NullBooleanField': 'bool', - 'OneToOneField': 'integer', - 'PositiveIntegerField': 'integer unsigned', - 'PositiveSmallIntegerField': 'smallint unsigned', - 'SlugField': 'varchar(%(max_length)s)', - 'SmallIntegerField': 'smallint', - 'TextField': 'text', - 'TimeField': 'time', + 'AutoField': 'integer', + 'BinaryField': 'BLOB', + 'BooleanField': 'bool', + 'CharField': 'varchar(%(max_length)s)', + 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'DecimalField': 'decimal', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'real', + 'IntegerField': 'integer', + 'BigIntegerField': 'bigint', + 'IPAddressField': 'char(15)', + 'GenericIPAddressField': 'char(39)', + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', + 'PositiveIntegerField': 'integer unsigned', + 'PositiveSmallIntegerField': 'smallint unsigned', + 'SlugField': 'varchar(%(max_length)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'text', + 'TimeField': 'time', } def sql_for_pending_references(self, model, style, pending_references): @@ -80,7 +82,6 @@ class DatabaseCreation(BaseDatabaseCreation): SQLite since the databases will be distinct despite having the same TEST_NAME. See http://www.sqlite.org/inmemorydb.html """ - settings_dict = self.connection.settings_dict test_dbname = self._get_test_db_name() sig = [self.connection.settings_dict['NAME']] if test_dbname == ':memory:': diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index 799d6ec8fd..2e674bc05b 100644 --- a/django/db/backends/sqlite3/introspection.py +++ b/django/db/backends/sqlite3/introspection.py @@ -1,8 +1,11 @@ import re + from django.db.backends import BaseDatabaseIntrospection, FieldInfo + field_size_re = re.compile(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$') + def get_field_size(name): """ Extract the size number from a "varchar(11)" type name """ m = field_size_re.search(name) @@ -46,6 +49,7 @@ class FlexibleFieldLookupDict(object): return ('CharField', {'max_length': size}) raise KeyError + class DatabaseIntrospection(BaseDatabaseIntrospection): data_types_reverse = FlexibleFieldLookupDict() @@ -76,7 +80,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): # Schema for this table cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"]) results = cursor.fetchone()[0].strip() - results = results[results.index('(')+1:results.rindex(')')] + results = results[results.index('(') + 1:results.rindex(')')] # Walk through and look for references to other tables. SQLite doesn't # really have enforced references, but since it echoes out the SQL used @@ -96,8 +100,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): result = cursor.fetchall()[0] other_table_results = result[0].strip() li, ri = other_table_results.index('('), other_table_results.rindex(')') - other_table_results = other_table_results[li+1:ri] - + other_table_results = other_table_results[li + 1:ri] for other_index, other_desc in enumerate(other_table_results.split(',')): other_desc = other_desc.strip() @@ -121,7 +124,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): # Schema for this table cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"]) results = cursor.fetchone()[0].strip() - results = results[results.index('(')+1:results.rindex(')')] + results = results[results.index('(') + 1:results.rindex(')')] # Walk through and look for references to other tables. SQLite doesn't # really have enforced references, but since it echoes out the SQL used @@ -166,7 +169,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): # Don't use PRAGMA because that causes issues with some transactions cursor.execute("SELECT sql FROM sqlite_master WHERE tbl_name = %s AND type = %s", [table_name, "table"]) results = cursor.fetchone()[0].strip() - results = results[results.index('(')+1:results.rindex(')')] + results = results[results.index('(') + 1:results.rindex(')')] for field_desc in results.split(','): field_desc = field_desc.strip() m = re.search('"(.*)".*PRIMARY KEY$', field_desc) diff --git a/django/db/backends/util.py b/django/db/backends/util.py index aa2601277a..f8157c50e7 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -85,20 +85,25 @@ class CursorDebugWrapper(CursorWrapper): def typecast_date(s): return datetime.date(*map(int, s.split('-'))) if s else None # returns None if s is null + def typecast_time(s): # does NOT store time zone information - if not s: return None + if not s: + return None hour, minutes, seconds = s.split(':') if '.' in seconds: # check whether seconds have a fractional part seconds, microseconds = seconds.split('.') else: microseconds = '0' - return datetime.time(int(hour), int(minutes), int(seconds), int(float('.'+microseconds) * 1000000)) + return datetime.time(int(hour), int(minutes), int(seconds), int(float('.' + microseconds) * 1000000)) + def typecast_timestamp(s): # does NOT store time zone information # "2005-07-29 15:48:00.590358-05" # "2005-07-29 09:56:00-05" - if not s: return None - if not ' ' in s: return typecast_date(s) + if not s: + return None + if not ' ' in s: + return typecast_date(s) d, t = s.split() # Extract timezone information, if it exists. Currently we just throw # it away, but in the future we may make use of it. @@ -122,11 +127,13 @@ def typecast_timestamp(s): # does NOT store time zone information int(times[0]), int(times[1]), int(seconds), int((microseconds + '000000')[:6]), tzinfo) + def typecast_decimal(s): if s is None or s == '': return None return decimal.Decimal(s) + ############################################### # Converters from Python to database (string) # ############################################### @@ -136,6 +143,7 @@ def rev_typecast_decimal(d): return None return str(d) + def truncate_name(name, length=None, hash_len=4): """Shortens a string to a repeatable mangled version with the given length. """ @@ -143,7 +151,8 @@ def truncate_name(name, length=None, hash_len=4): return name hsh = hashlib.md5(force_bytes(name)).hexdigest()[:hash_len] - return '%s%s' % (name[:length-hash_len], hsh) + return '%s%s' % (name[:length - hash_len], hsh) + def format_number(value, max_digits, decimal_places): """ diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index b5dd1a58bc..4d310e480b 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -26,6 +26,7 @@ def permalink(func): (viewname, viewargs, viewkwargs) """ from django.core.urlresolvers import reverse + @wraps(func) def inner(*args, **kwargs): bits = func(*args, **kwargs) diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py index b89db1c563..1db3890204 100644 --- a/django/db/models/aggregates.py +++ b/django/db/models/aggregates.py @@ -3,6 +3,7 @@ Classes to represent the definitions of aggregate functions. """ from django.db.models.constants import LOOKUP_SEP + def refs_aggregate(lookup_parts, aggregates): """ A little helper method to check if the lookup_parts contains references @@ -15,6 +16,7 @@ def refs_aggregate(lookup_parts, aggregates): return True return False + class Aggregate(object): """ Default Aggregate definition. @@ -58,23 +60,30 @@ class Aggregate(object): aggregate = klass(col, source=source, is_summary=is_summary, **self.extra) query.aggregates[alias] = aggregate + class Avg(Aggregate): name = 'Avg' + class Count(Aggregate): name = 'Count' + class Max(Aggregate): name = 'Max' + class Min(Aggregate): name = 'Min' + class StdDev(Aggregate): name = 'StdDev' + class Sum(Aggregate): name = 'Sum' + class Variance(Aggregate): name = 'Variance' diff --git a/django/db/models/base.py b/django/db/models/base.py index 17a4a14b8d..a2937f12c6 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -19,6 +19,7 @@ from django.db.models.query_utils import DeferredAttribute, deferred_class_facto from django.db.models.deletion import Collector from django.db.models.options import Options from django.db.models import signals +from django.db.models.loading import register_models, get_model, MODELS_MODULE_NAME from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry from django.utils.encoding import force_str, force_text @@ -85,10 +86,22 @@ class ModelBase(type): base_meta = getattr(new_class, '_meta', None) if getattr(meta, 'app_label', None) is None: - # Figure out the app_label by looking one level up. + # Figure out the app_label by looking one level up from the package + # or module named 'models'. If no such package or module exists, + # fall back to looking one level up from the module this model is + # defined in. + # For 'django.contrib.sites.models', this would be 'sites'. + # For 'geo.models.places' this would be 'geo'. + model_module = sys.modules[new_class.__module__] - kwargs = {"app_label": model_module.__name__.split('.')[-2]} + package_components = model_module.__name__.split('.') + package_components.reverse() # find the last occurrence of 'models' + try: + app_label_index = package_components.index(MODELS_MODULE_NAME) + 1 + except ValueError: + app_label_index = 1 + kwargs = {"app_label": package_components[app_label_index]} else: kwargs = {} @@ -225,9 +238,9 @@ class ModelBase(type): # class for field in base._meta.virtual_fields: if base._meta.abstract and field.name in field_names: - raise FieldError('Local field %r in class %r clashes '\ - 'with field of similar name from '\ - 'abstract base class %r' % \ + raise FieldError('Local field %r in class %r clashes ' + 'with field of similar name from ' + 'abstract base class %r' % (field.name, name, base.__name__)) new_class.add_to_class(field.name, copy.deepcopy(field)) @@ -1007,8 +1020,6 @@ def get_absolute_url(opts, func, self, *args, **kwargs): # MISC # ######## -class Empty(object): - pass def simple_class_factory(model, attrs): """ @@ -1016,6 +1027,7 @@ def simple_class_factory(model, attrs): """ return model + def model_unpickle(model_id, attrs, factory): """ Used to unpickle Model subclasses with deferred fields. diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 6e0f3c434e..b4eea5fe71 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -4,6 +4,7 @@ from django.db.models.aggregates import refs_aggregate from django.db.models.constants import LOOKUP_SEP from django.utils import tree + class ExpressionNode(tree.Node): """ Base class for all query expressions. @@ -128,6 +129,7 @@ class ExpressionNode(tree.Node): "Use .bitand() and .bitor() for bitwise logical operations." ) + class F(ExpressionNode): """ An expression representing the value of the given field. @@ -147,6 +149,7 @@ class F(ExpressionNode): def evaluate(self, evaluator, qn, connection): return evaluator.evaluate_leaf(self, qn, connection) + class DateModifierNode(ExpressionNode): """ Node that implements the following syntax: diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 9263f81184..0410815c92 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import collections import copy import datetime import decimal @@ -17,7 +18,6 @@ from django.core import exceptions, validators from django.utils.datastructures import DictWrapper from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.functional import curry, total_ordering -from django.utils.itercompat import is_iterator from django.utils.text import capfirst from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -25,9 +25,11 @@ from django.utils.encoding import smart_text, force_text, force_bytes from django.utils.ipv6 import clean_ipv6_address from django.utils import six + class Empty(object): pass + class NOT_PROVIDED: pass @@ -35,12 +37,15 @@ class NOT_PROVIDED: # of most "choices" lists. BLANK_CHOICE_DASH = [("", "---------")] + def _load_field(app_label, model_name, field_name): return get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0] + class FieldDoesNotExist(Exception): pass + # A guide to Field parameters: # # * name: The name of the field specifed in the model. @@ -61,6 +66,7 @@ def _empty(of_cls): new.__class__ = of_cls return new + @total_ordering class Field(object): """Base class for all field types""" @@ -464,12 +470,12 @@ class Field(object): if hasattr(value, '_prepare'): return value._prepare() - if lookup_type in ( - 'iexact', 'contains', 'icontains', - 'startswith', 'istartswith', 'endswith', 'iendswith', - 'month', 'day', 'week_day', 'hour', 'minute', 'second', - 'isnull', 'search', 'regex', 'iregex', - ): + if lookup_type in { + 'iexact', 'contains', 'icontains', + 'startswith', 'istartswith', 'endswith', 'iendswith', + 'month', 'day', 'week_day', 'hour', 'minute', 'second', + 'isnull', 'search', 'regex', 'iregex', + }: return value elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte'): return self.get_prep_value(value) @@ -599,7 +605,7 @@ class Field(object): return bound_field_class(self, fieldmapping, original) def _get_choices(self): - if is_iterator(self._choices): + if isinstance(self._choices, collections.Iterator): choices, self._choices = tee(self._choices) return choices else: @@ -613,7 +619,7 @@ class Field(object): if isinstance(value, (list, tuple)): flat.extend(value) else: - flat.append((choice,value)) + flat.append((choice, value)) return flat flatchoices = property(_get_flatchoices) @@ -732,6 +738,7 @@ class AutoField(Field): def formfield(self, **kwargs): return None + class BooleanField(Field): empty_strings_allowed = False default_error_messages = { @@ -786,13 +793,13 @@ class BooleanField(Field): if self.choices: include_blank = (self.null or not (self.has_default() or 'initial' in kwargs)) - defaults = {'choices': self.get_choices( - include_blank=include_blank)} + defaults = {'choices': self.get_choices(include_blank=include_blank)} else: defaults = {'form_class': forms.BooleanField} defaults.update(kwargs) return super(BooleanField, self).formfield(**defaults) + class CharField(Field): description = _("String (up to %(max_length)s)") @@ -819,6 +826,7 @@ class CharField(Field): defaults.update(kwargs) return super(CharField, self).formfield(**defaults) + # TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): default_validators = [validators.validate_comma_separated_integer_list] @@ -833,6 +841,7 @@ class CommaSeparatedIntegerField(CharField): defaults.update(kwargs) return super(CommaSeparatedIntegerField, self).formfield(**defaults) + class DateField(Field): empty_strings_allowed = False default_error_messages = { @@ -905,7 +914,7 @@ class DateField(Field): return super(DateField, self).pre_save(model_instance, add) def contribute_to_class(self, cls, name): - super(DateField,self).contribute_to_class(cls, name) + super(DateField, self).contribute_to_class(cls, name) if not self.null: setattr(cls, 'get_next_by_%s' % self.name, curry(cls._get_next_or_previous_by_FIELD, field=self, @@ -939,6 +948,7 @@ class DateField(Field): defaults.update(kwargs) return super(DateField, self).formfield(**defaults) + class DateTimeField(DateField): empty_strings_allowed = False default_error_messages = { @@ -1045,6 +1055,7 @@ class DateTimeField(DateField): defaults.update(kwargs) return super(DateTimeField, self).formfield(**defaults) + class DecimalField(Field): empty_strings_allowed = False default_error_messages = { @@ -1116,6 +1127,7 @@ class DecimalField(Field): defaults.update(kwargs) return super(DecimalField, self).formfield(**defaults) + class EmailField(CharField): default_validators = [validators.validate_email] description = _("Email address") @@ -1142,13 +1154,14 @@ class EmailField(CharField): defaults.update(kwargs) return super(EmailField, self).formfield(**defaults) + class FilePathField(Field): description = _("File path") def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, allow_files=True, allow_folders=False, **kwargs): self.path, self.match, self.recursive = path, match, recursive - self.allow_files, self.allow_folders = allow_files, allow_folders + self.allow_files, self.allow_folders = allow_files, allow_folders kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) @@ -1183,6 +1196,7 @@ class FilePathField(Field): def get_internal_type(self): return "FilePathField" + class FloatField(Field): empty_strings_allowed = False default_error_messages = { @@ -1215,6 +1229,7 @@ class FloatField(Field): defaults.update(kwargs) return super(FloatField, self).formfield(**defaults) + class IntegerField(Field): empty_strings_allowed = False default_error_messages = { @@ -1253,6 +1268,7 @@ class IntegerField(Field): defaults.update(kwargs) return super(IntegerField, self).formfield(**defaults) + class BigIntegerField(IntegerField): empty_strings_allowed = False description = _("Big (8 byte) integer") @@ -1267,6 +1283,7 @@ class BigIntegerField(IntegerField): defaults.update(kwargs) return super(BigIntegerField, self).formfield(**defaults) + class IPAddressField(Field): empty_strings_allowed = False description = _("IPv4 address") @@ -1288,6 +1305,7 @@ class IPAddressField(Field): defaults.update(kwargs) return super(IPAddressField, self).formfield(**defaults) + class GenericIPAddressField(Field): empty_strings_allowed = True description = _("IP address") @@ -1336,7 +1354,10 @@ class GenericIPAddressField(Field): return value def formfield(self, **kwargs): - defaults = {'form_class': forms.GenericIPAddressField} + defaults = { + 'protocol': self.protocol, + 'form_class': forms.GenericIPAddressField, + } defaults.update(kwargs) return super(GenericIPAddressField, self).formfield(**defaults) @@ -1403,6 +1424,7 @@ class NullBooleanField(Field): defaults.update(kwargs) return super(NullBooleanField, self).formfield(**defaults) + class PositiveIntegerField(IntegerField): description = _("Positive integer") @@ -1414,6 +1436,7 @@ class PositiveIntegerField(IntegerField): defaults.update(kwargs) return super(PositiveIntegerField, self).formfield(**defaults) + class PositiveSmallIntegerField(IntegerField): description = _("Positive small integer") @@ -1425,6 +1448,7 @@ class PositiveSmallIntegerField(IntegerField): defaults.update(kwargs) return super(PositiveSmallIntegerField, self).formfield(**defaults) + class SlugField(CharField): default_validators = [validators.validate_slug] description = _("Slug (up to %(max_length)s)") @@ -1454,12 +1478,14 @@ class SlugField(CharField): defaults.update(kwargs) return super(SlugField, self).formfield(**defaults) + class SmallIntegerField(IntegerField): description = _("Small integer") def get_internal_type(self): return "SmallIntegerField" + class TextField(Field): description = _("Text") @@ -1476,6 +1502,7 @@ class TextField(Field): defaults.update(kwargs) return super(TextField, self).formfield(**defaults) + class TimeField(Field): empty_strings_allowed = False default_error_messages = { @@ -1559,6 +1586,7 @@ class TimeField(Field): defaults.update(kwargs) return super(TimeField, self).formfield(**defaults) + class URLField(CharField): default_validators = [validators.URLValidator()] description = _("URL") @@ -1582,6 +1610,7 @@ class URLField(CharField): defaults.update(kwargs) return super(URLField, self).formfield(**defaults) + class BinaryField(Field): description = _("Raw binary data") empty_values = [None, b''] diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 7663c8ab90..311f74a905 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -11,6 +11,7 @@ from django.utils.encoding import force_str, force_text from django.utils import six from django.utils.translation import ugettext_lazy as _ + class FieldFile(File): def __init__(self, instance, field, name): super(FieldFile, self).__init__(None, name) @@ -135,6 +136,7 @@ class FieldFile(File): # be restored later, by FileDescriptor below. return {'name': self.name, 'closed': False, '_committed': True, '_file': None} + class FileDescriptor(object): """ The descriptor for the file attribute on the model instance. Returns a @@ -205,6 +207,7 @@ class FileDescriptor(object): def __set__(self, instance, value): instance.__dict__[self.field.name] = value + class FileField(Field): # The class to wrap instance attributes in. Accessing the file object off @@ -300,6 +303,7 @@ class FileField(Field): defaults.update(kwargs) return super(FileField, self).formfield(**defaults) + class ImageFileDescriptor(FileDescriptor): """ Just like the FileDescriptor, but for ImageFields. The only difference is @@ -321,14 +325,15 @@ class ImageFileDescriptor(FileDescriptor): if previous_file is not None: self.field.update_dimension_fields(instance, force=True) -class ImageFieldFile(ImageFile, FieldFile): +class ImageFieldFile(ImageFile, FieldFile): def delete(self, save=True): # Clear the image dimensions cache if hasattr(self, '_dimensions_cache'): del self._dimensions_cache super(ImageFieldFile, self).delete(save) + class ImageField(FileField): attr_class = ImageFieldFile descriptor_class = ImageFileDescriptor diff --git a/django/db/models/fields/proxy.py b/django/db/models/fields/proxy.py index c0cc873f4c..29c782b59a 100644 --- a/django/db/models/fields/proxy.py +++ b/django/db/models/fields/proxy.py @@ -5,6 +5,7 @@ have the same attributes as fields sometimes (avoids a lot of special casing). from django.db.models import fields + class OrderWrt(fields.IntegerField): """ A proxy for the _order database field that is used when diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 480c77ab77..5a7718e880 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -211,7 +211,7 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri # If null=True, we can assign null here, but otherwise the value needs # to be an instance of the related class. - if value is None and self.related.field.null == False: + if value is None and self.related.field.null is False: raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' % (instance._meta.object_name, self.related.get_accessor_name())) elif value is not None and not isinstance(value, self.related.model): @@ -310,7 +310,7 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec def __set__(self, instance, value): # If null=True, we can assign null here, but otherwise the value needs # to be an instance of the related class. - if value is None and self.field.null == False: + if value is None and self.field.null is False: raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' % (instance._meta.object_name, self.field.name)) elif value is not None and not isinstance(value, self.field.rel.to): @@ -395,7 +395,7 @@ class ForeignRelatedObjectsDescriptor(object): def __init__(self, instance): super(RelatedManager, self).__init__() self.instance = instance - self.core_filters= {'%s__exact' % rel_field.name: instance} + self.core_filters = {'%s__exact' % rel_field.name: instance} self.model = rel_model def get_queryset(self): @@ -510,7 +510,6 @@ def create_many_related_manager(superclass, rel): "a many-to-many relationship can be used." % instance.__class__.__name__) - def _get_fk_val(self, obj, field_name): """ Returns the correct value for this relationship's foreign key. This @@ -821,6 +820,7 @@ class ReverseManyRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) + class ForeignObjectRel(object): def __init__(self, field, to, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None, related_query_name=None): @@ -858,6 +858,7 @@ class ForeignObjectRel(object): # example custom multicolumn joins currently have no remote field). self.field_name = None + class ManyToOneRel(ForeignObjectRel): def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None, related_query_name=None): @@ -1125,7 +1126,7 @@ class ForeignKey(ForeignObject): def __init__(self, to, to_field=None, rel_class=ManyToOneRel, db_constraint=True, **kwargs): try: - to_name = to._meta.object_name.lower() + to._meta.object_name.lower() except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: @@ -1162,7 +1163,6 @@ class ForeignKey(ForeignObject): if self.rel.on_delete is not CASCADE: kwargs['on_delete'] = self.rel.on_delete # Rel needs more work. - rel = self.rel if self.rel.field_name: kwargs['to_field'] = self.rel.field_name if isinstance(self.rel.to, six.string_types): @@ -1193,8 +1193,8 @@ class ForeignKey(ForeignObject): using = router.db_for_read(model_instance.__class__, instance=model_instance) qs = self.rel.to._default_manager.using(using).filter( - **{self.rel.field_name: value} - ) + **{self.rel.field_name: value} + ) qs = qs.complex_filter(self.rel.limit_choices_to) if not qs.exists(): raise exceptions.ValidationError( @@ -1222,7 +1222,7 @@ class ForeignKey(ForeignObject): return field_default def get_db_prep_save(self, value, connection): - if value == '' or value == None: + if value == '' or value is None: return None else: return self.related_field.get_db_prep_save(value, @@ -1393,7 +1393,6 @@ class ManyToManyField(RelatedField): if "help_text" in kwargs: del kwargs['help_text'] # Rel needs more work. - rel = self.rel if isinstance(self.rel.to, six.string_types): kwargs['to'] = self.rel.to else: diff --git a/django/db/models/fields/subclassing.py b/django/db/models/fields/subclassing.py index e6153aefe0..591adb7121 100644 --- a/django/db/models/fields/subclassing.py +++ b/django/db/models/fields/subclassing.py @@ -7,6 +7,7 @@ to_python() and the other necessary methods and everything will work seamlessly. """ + class SubfieldBase(type): """ A metaclass for custom Field subclasses. This ensures the model's attribute @@ -19,6 +20,7 @@ class SubfieldBase(type): ) return new_class + class Creator(object): """ A placeholder class that provides a way to set the attribute on the model. @@ -34,6 +36,7 @@ class Creator(object): def __set__(self, obj, value): obj.__dict__[self.field.name] = self.field.to_python(value) + def make_contrib(superclass, func=None): """ Returns a suitable contribute_to_class() method for the Field subclass. diff --git a/django/db/models/loading.py b/django/db/models/loading.py index c1e394327f..df3be5fe9b 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -15,9 +15,13 @@ import os __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') +MODELS_MODULE_NAME = 'models' + + class UnavailableApp(Exception): pass + def _initialize(): """ Returns a dictionary to be used as the initial value of the @@ -118,12 +122,12 @@ class BaseAppCache(object): self.nesting_level += 1 app_module = import_module(app_name) try: - models = import_module('.models', app_name) + models = import_module('.' + MODELS_MODULE_NAME, app_name) except ImportError: self.nesting_level -= 1 # If the app doesn't have a models module, we can just ignore the # ImportError and return no models for it. - if not module_has_submodule(app_module, 'models'): + if not module_has_submodule(app_module, MODELS_MODULE_NAME): return None # But if the app does have a models module, we need to figure out # whether to suppress or propagate the error. If can_postpone is diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 6817c9c8ee..b369aedb64 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -6,6 +6,7 @@ from django.db.models.fields import FieldDoesNotExist from django.utils import six from django.utils.deprecation import RenameMethodsBase + def ensure_default_manager(sender, **kwargs): """ Ensures that a Model subclass contains a default manager and sets the @@ -153,6 +154,9 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def get_or_create(self, **kwargs): return self.get_queryset().get_or_create(**kwargs) + def update_or_create(self, **kwargs): + return self.get_queryset().update_or_create(**kwargs) + def create(self, **kwargs): return self.get_queryset().create(**kwargs) @@ -245,7 +249,7 @@ class ManagerDescriptor(object): self.manager = manager def __get__(self, instance, type=None): - if instance != None: + if instance is not None: raise AttributeError("Manager isn't accessible via %s instances" % type.__name__) return self.manager diff --git a/django/db/models/query.py b/django/db/models/query.py index 34d8eaa6ff..811e917764 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -19,6 +19,10 @@ from django.utils.functional import partition from django.utils import six from django.utils import timezone +# The maximum number (one less than the max to be precise) of results to fetch +# in a get() query +MAX_GET_RESULTS = 20 + # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 @@ -297,6 +301,7 @@ class QuerySet(object): clone = self.filter(*args, **kwargs) if self.query.can_filter(): clone = clone.order_by() + clone = clone[:MAX_GET_RESULTS + 1] num = len(clone) if num == 1: return clone._result_cache[0] @@ -305,8 +310,11 @@ class QuerySet(object): "%s matching query does not exist." % self.model._meta.object_name) raise self.model.MultipleObjectsReturned( - "get() returned more than one %s -- it returned %s!" % - (self.model._meta.object_name, num)) + "get() returned more than one %s -- it returned %s!" % ( + self.model._meta.object_name, + num if num <= MAX_GET_RESULTS else 'more than %s' % MAX_GET_RESULTS + ) + ) def create(self, **kwargs): """ @@ -351,42 +359,89 @@ class QuerySet(object): if objs_with_pk: self._batched_insert(objs_with_pk, fields, batch_size) if objs_without_pk: - fields= [f for f in fields if not isinstance(f, AutoField)] + fields = [f for f in fields if not isinstance(f, AutoField)] self._batched_insert(objs_without_pk, fields, batch_size) return objs - def get_or_create(self, **kwargs): + def get_or_create(self, defaults=None, **kwargs): """ Looks up an object with the given kwargs, creating one if necessary. Returns a tuple of (object, created), where created is a boolean specifying whether an object was created. """ - defaults = kwargs.pop('defaults', {}) - lookup = kwargs.copy() - for f in self.model._meta.fields: - if f.attname in lookup: - lookup[f.name] = lookup.pop(f.attname) + lookup, params, _ = self._extract_model_params(defaults, **kwargs) try: self._for_write = True return self.get(**lookup), False except self.model.DoesNotExist: + return self._create_object_from_params(lookup, params) + + def update_or_create(self, defaults=None, **kwargs): + """ + Looks up an object with the given kwargs, updating one with defaults + if it exists, otherwise creates a new one. + Returns a tuple (object, created), where created is a boolean + specifying whether an object was created. + """ + lookup, params, filtered_defaults = self._extract_model_params(defaults, **kwargs) + try: + self._for_write = True + obj = self.get(**lookup) + except self.model.DoesNotExist: + obj, created = self._create_object_from_params(lookup, params) + if created: + return obj, created + for k, v in six.iteritems(filtered_defaults): + setattr(obj, k, v) + try: + sid = transaction.savepoint(using=self.db) + obj.save(update_fields=filtered_defaults.keys(), using=self.db) + transaction.savepoint_commit(sid, using=self.db) + return obj, False + except DatabaseError: + transaction.savepoint_rollback(sid, using=self.db) + six.reraise(sys.exc_info()) + + def _create_object_from_params(self, lookup, params): + """ + Tries to create an object using passed params. + Used by get_or_create and update_or_create + """ + try: + obj = self.model(**params) + sid = transaction.savepoint(using=self.db) + obj.save(force_insert=True, using=self.db) + transaction.savepoint_commit(sid, using=self.db) + return obj, True + except DatabaseError: + transaction.savepoint_rollback(sid, using=self.db) + exc_info = sys.exc_info() try: - params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k) - params.update(defaults) - obj = self.model(**params) - sid = transaction.savepoint(using=self.db) - obj.save(force_insert=True, using=self.db) - transaction.savepoint_commit(sid, using=self.db) - return obj, True - except DatabaseError: - transaction.savepoint_rollback(sid, using=self.db) - exc_info = sys.exc_info() - try: - return self.get(**lookup), False - except self.model.DoesNotExist: - # Re-raise the DatabaseError with its original traceback. - six.reraise(*exc_info) + return self.get(**lookup), False + except self.model.DoesNotExist: + # Re-raise the DatabaseError with its original traceback. + six.reraise(*exc_info) + + def _extract_model_params(self, defaults, **kwargs): + """ + Prepares `lookup` (kwargs that are valid model attributes), `params` + (for creating a model instance) and `filtered_defaults` (defaults + that are valid model attributes) based on given kwargs; for use by + get_or_create and update_or_create. + """ + defaults = defaults or {} + filtered_defaults = {} + lookup = kwargs.copy() + for f in self.model._meta.fields: + # Filter out fields that don't belongs to the model. + if f.attname in lookup: + lookup[f.name] = lookup.pop(f.attname) + if f.attname in defaults: + filtered_defaults[f.name] = defaults.pop(f.attname) + params = dict((k, v) for k, v in kwargs.items() if LOOKUP_SEP not in k) + params.update(filtered_defaults) + return lookup, params, filtered_defaults def _earliest_or_latest(self, field_name=None, direction="-"): """ @@ -638,7 +693,7 @@ class QuerySet(object): obj.query.select_for_update_nowait = nowait return obj - def select_related(self, *fields, **kwargs): + def select_related(self, *fields): """ Returns a new QuerySet instance that will select related objects. @@ -647,9 +702,6 @@ class QuerySet(object): If select_related(None) is called, the list is cleared. """ - if kwargs: - raise TypeError('Unexpected keyword arguments to select_related: %s' - % (list(kwargs),)) obj = self._clone() if fields == (None,): obj.query.select_related = False @@ -821,7 +873,7 @@ class QuerySet(object): return ops = connections[self.db].ops batch_size = (batch_size or max(ops.bulk_batch_size(fields, objs), 1)) - for batch in [objs[i:i+batch_size] + for batch in [objs[i:i + batch_size] for i in range(0, len(objs), batch_size)]: self.model._base_manager._insert(batch, fields=fields, using=self.db) @@ -902,10 +954,12 @@ class QuerySet(object): # empty" result. value_annotation = True + class InstanceCheckMeta(type): def __instancecheck__(self, instance): return instance.query.is_empty() + class EmptyQuerySet(six.with_metaclass(InstanceCheckMeta)): """ Marker class usable for checking if a queryset is empty by .none(): @@ -915,6 +969,7 @@ class EmptyQuerySet(six.with_metaclass(InstanceCheckMeta)): def __init__(self, *args, **kwargs): raise TypeError("EmptyQuerySet can't be instantiated") + class ValuesQuerySet(QuerySet): def __init__(self, *args, **kwargs): super(ValuesQuerySet, self).__init__(*args, **kwargs) @@ -1243,7 +1298,7 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None, only_load.get(o.model), reverse=True): next = requested[o.field.related_query_name()] parent = klass if issubclass(o.model, klass) else None - klass_info = get_klass_info(o.model, max_depth=max_depth, cur_depth=cur_depth+1, + klass_info = get_klass_info(o.model, max_depth=max_depth, cur_depth=cur_depth + 1, requested=next, only_load=only_load, from_parent=parent) reverse_related_fields.append((o.field, klass_info)) if field_names: @@ -1254,7 +1309,7 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None, return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx -def get_cached_row(row, index_start, using, klass_info, offset=0, +def get_cached_row(row, index_start, using, klass_info, offset=0, parent_data=()): """ Helper function that recursively returns an object with the specified @@ -1279,11 +1334,10 @@ def get_cached_row(row, index_start, using, klass_info, offset=0, return None klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx = klass_info - - fields = row[index_start : index_start + field_count] + fields = row[index_start:index_start + field_count] # If the pk column is None (or the Oracle equivalent ''), then the related # object must be non-existent - set the relation to None. - if fields[pk_idx] == None or fields[pk_idx] == '': + if fields[pk_idx] is None or fields[pk_idx] == '': obj = None elif field_names: fields = list(fields) @@ -1513,8 +1567,6 @@ def prefetch_related_objects(result_cache, related_lookups): if len(result_cache) == 0: return # nothing to do - model = result_cache[0].__class__ - # We need to be able to dynamically add to the list of prefetch_related # lookups that we look up (see below). So we need some book keeping to # ensure we don't do duplicate work. @@ -1541,7 +1593,7 @@ def prefetch_related_objects(result_cache, related_lookups): if len(obj_list) == 0: break - current_lookup = LOOKUP_SEP.join(attrs[0:level+1]) + current_lookup = LOOKUP_SEP.join(attrs[:level + 1]) if current_lookup in done_queries: # Skip any prefetching, and any object preparation obj_list = done_queries[current_lookup] diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index ee7a56a26c..2a92978beb 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -30,6 +30,7 @@ class QueryWrapper(object): def as_sql(self, qn=None, connection=None): return self.data + class Q(tree.Node): """ Encapsulates filters as objects that can then be combined logically (using @@ -74,6 +75,7 @@ class Q(tree.Node): clone.children.append(child) return clone + class DeferredAttribute(object): """ A wrapper for a deferred-loading field. When the value is read from this @@ -99,8 +101,7 @@ class DeferredAttribute(object): try: f = opts.get_field_by_name(self.field_name)[0] except FieldDoesNotExist: - f = [f for f in opts.fields - if f.attname == self.field_name][0] + f = [f for f in opts.fields if f.attname == self.field_name][0] name = f.name # Let's see if the field is part of the parent chain. If so we # might be able to reuse the already loaded value. Refs #18343. @@ -174,6 +175,7 @@ def select_related_descend(field, restricted, requested, load_fields, reverse=Fa return False return True + # This function is needed because data descriptors must be defined on a class # object, not an instance, to have any effect. diff --git a/django/db/models/related.py b/django/db/models/related.py index 4b00dd343b..ba2a90c545 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -10,6 +10,7 @@ PathInfo = namedtuple('PathInfo', 'from_opts to_opts target_fields join_field ' 'm2m direct') + class RelatedObject(object): def __init__(self, parent_model, model, field): self.parent_model = parent_model diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index 2bd2b2f76f..9fc5fe8a5b 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -9,6 +9,7 @@ from django.db.models.fields import IntegerField, FloatField ordinal_aggregate_field = IntegerField() computed_aggregate_field = FloatField() + class Aggregate(object): """ Default SQL Aggregate. @@ -93,6 +94,7 @@ class Avg(Aggregate): is_computed = True sql_function = 'AVG' + class Count(Aggregate): is_ordinal = True sql_function = 'COUNT' @@ -101,12 +103,15 @@ class Count(Aggregate): def __init__(self, col, distinct=False, **extra): super(Count, self).__init__(col, distinct='DISTINCT ' if distinct else '', **extra) + class Max(Aggregate): sql_function = 'MAX' + class Min(Aggregate): sql_function = 'MIN' + class StdDev(Aggregate): is_computed = True @@ -114,9 +119,11 @@ class StdDev(Aggregate): super(StdDev, self).__init__(col, **extra) self.sql_function = 'STDDEV_SAMP' if sample else 'STDDEV_POP' + class Sum(Aggregate): sql_function = 'SUM' + class Variance(Aggregate): is_computed = True diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index f70750abed..e17cb3f616 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -688,7 +688,7 @@ class SQLCompiler(object): # the relation, which is always nullable. new_nullable = True table = model._meta.db_table - self.fill_related_selections(model._meta, table, cur_depth+1, + self.fill_related_selections(model._meta, table, cur_depth + 1, next, restricted, new_nullable) def deferred_to_columns(self): @@ -750,6 +750,16 @@ class SQLCompiler(object): yield row + def has_results(self): + """ + Backends (e.g. NoSQL) can override this in order to use optimized + versions of "query has any results." + """ + # This is always executed on a query clone, so we can modify self.query + self.query.add_extra({'a': 1}, None, None, None, None, None) + self.query.set_extra_mask(['a']) + return bool(self.execute_sql(SINGLE)) + def execute_sql(self, result_type=MULTI): """ Run the query against the database and returns the result(s). The @@ -915,6 +925,7 @@ class SQLDeleteCompiler(SQLCompiler): result.append('WHERE %s' % where) return ' '.join(result), tuple(params) + class SQLUpdateCompiler(SQLCompiler): def as_sql(self): """ @@ -1029,6 +1040,7 @@ class SQLUpdateCompiler(SQLCompiler): for alias in self.query.tables[1:]: self.query.alias_refcount[alias] = 0 + class SQLAggregateCompiler(SQLCompiler): def as_sql(self, qn=None): """ @@ -1050,6 +1062,7 @@ class SQLAggregateCompiler(SQLCompiler): params = params + self.query.sub_params return sql, params + class SQLDateCompiler(SQLCompiler): def results_iter(self): """ @@ -1075,6 +1088,7 @@ class SQLDateCompiler(SQLCompiler): date = date.date() yield date + class SQLDateTimeCompiler(SQLCompiler): def results_iter(self): """ @@ -1107,6 +1121,7 @@ class SQLDateTimeCompiler(SQLCompiler): datetime = timezone.make_aware(datetime, self.query.tzinfo) yield datetime + def order_modified_iter(cursor, trim, sentinel): """ Yields blocks of rows from a cursor. We use this iterator in the special diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py index daaabbe6da..76b3db5b35 100644 --- a/django/db/models/sql/datastructures.py +++ b/django/db/models/sql/datastructures.py @@ -3,9 +3,11 @@ Useful auxilliary data structures for query construction. Not useful outside the SQL domain. """ + class EmptyResultSet(Exception): pass + class MultiJoin(Exception): """ Used by join construction code to indicate the point at which a @@ -17,12 +19,10 @@ class MultiJoin(Exception): # The path travelled, this includes the path to the multijoin. self.names_with_path = path_with_names + class Empty(object): pass -class RawValue(object): - def __init__(self, value): - self.value = value class Date(object): """ @@ -42,6 +42,7 @@ class Date(object): col = self.col return connection.ops.date_trunc_sql(self.lookup_type, col), [] + class DateTime(object): """ Add a datetime selection column. diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index 62adf79d87..f9a8929974 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -1,7 +1,9 @@ +import copy + from django.core.exceptions import FieldError from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import FieldDoesNotExist -import copy + class SQLEvaluator(object): def __init__(self, expression, query, allow_joins=True, reuse=None): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 75e8e7540d..2a9b8ef826 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -401,12 +401,10 @@ class Query(object): def has_results(self, using): q = self.clone() q.clear_select_clause() - q.add_extra({'a': 1}, None, None, None, None, None) - q.set_extra_mask(['a']) q.clear_ordering(True) q.set_limits(high=1) compiler = q.get_compiler(using=using) - return bool(compiler.execute_sql(SINGLE)) + return compiler.has_results() def combine(self, rhs, connector): """ @@ -615,7 +613,6 @@ class Query(object): for model, values in six.iteritems(seen): callback(target, model, values) - def deferred_to_columns_cb(self, target, model, fields): """ Callback used by deferred_to_columns(). The "target" parameter should @@ -627,7 +624,6 @@ class Query(object): for field in fields: target[table].add(field.column) - def table_alias(self, table_name, create=False): """ Returns a table alias for the given table_name and whether this is a @@ -955,7 +951,6 @@ class Query(object): self.unref_alias(alias) self.included_inherited_models = {} - def add_aggregate(self, aggregate, model, alias, is_summary): """ Adds a single aggregate expression to the Query @@ -1780,7 +1775,7 @@ class Query(object): return self._aggregate_select_cache elif self.aggregate_select_mask is not None: self._aggregate_select_cache = SortedDict([ - (k,v) for k,v in self.aggregates.items() + (k, v) for k, v in self.aggregates.items() if k in self.aggregate_select_mask ]) return self._aggregate_select_cache @@ -1793,7 +1788,7 @@ class Query(object): return self._extra_select_cache elif self.extra_select_mask is not None: self._extra_select_cache = SortedDict([ - (k,v) for k,v in self.extra.items() + (k, v) for k, v in self.extra.items() if k in self.extra_select_mask ]) return self._extra_select_cache @@ -1876,6 +1871,7 @@ class Query(object): else: return field.null + def get_order_dir(field, default='ASC'): """ Returns the field name and direction for an order specification. For @@ -1900,6 +1896,7 @@ def add_to_dict(data, key, value): else: data[key] = set([value]) + def is_reverse_o2o(field): """ A little helper to check if the given field is reverse-o2o. The field is @@ -1907,6 +1904,7 @@ def is_reverse_o2o(field): """ return not hasattr(field, 'rel') and field.field.unique + def alias_diff(refcounts_before, refcounts_after): """ Given the before and after copies of refcounts works out which aliases diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 20770162c9..6aab02bd9a 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -7,7 +7,7 @@ from django.core.exceptions import FieldError from django.db import connections from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import DateField, DateTimeField, FieldDoesNotExist -from django.db.models.sql.constants import * +from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, SelectInfo from django.db.models.sql.datastructures import Date, DateTime from django.db.models.sql.query import Query from django.db.models.sql.where import AND, Constraint @@ -20,6 +20,7 @@ from django.utils import timezone __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', 'DateTimeQuery', 'AggregateQuery'] + class DeleteQuery(Query): """ Delete queries are done through this class, since they are more constrained @@ -77,7 +78,9 @@ class DeleteQuery(Query): return else: innerq.clear_select_clause() - innerq.select = [SelectInfo((self.get_initial_alias(), pk.column), None)] + innerq.select = [ + SelectInfo((self.get_initial_alias(), pk.column), None) + ] values = innerq where = self.where_class() where.add((Constraint(None, pk.column, pk), 'in', values), AND) @@ -178,6 +181,7 @@ class UpdateQuery(Query): result.append(query) return result + class InsertQuery(Query): compiler = 'SQLInsertCompiler' @@ -215,6 +219,7 @@ class InsertQuery(Query): self.objs = objs self.raw = raw + class DateQuery(Query): """ A DateQuery is a normal query, except that it specifically selects a single @@ -260,6 +265,7 @@ class DateQuery(Query): def _get_select(self, col, lookup_type): return Date(col, lookup_type) + class DateTimeQuery(DateQuery): """ A DateTimeQuery is like a DateQuery but for a datetime field. If time zone @@ -280,6 +286,7 @@ class DateTimeQuery(DateQuery): tzname = timezone._get_timezone_name(self.tzinfo) return DateTime(col, lookup_type, tzname) + class AggregateQuery(Query): """ An AggregateQuery takes another query as a parameter to the FROM diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 2a342d417a..2e83ecdce4 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -4,6 +4,7 @@ Code to manage the creation and SQL rendering of 'where' constraints. from __future__ import absolute_import +import collections import datetime from itertools import repeat @@ -11,15 +12,16 @@ from django.conf import settings from django.db.models.fields import DateTimeField, Field from django.db.models.sql.datastructures import EmptyResultSet, Empty from django.db.models.sql.aggregates import Aggregate -from django.utils.itercompat import is_iterator from django.utils.six.moves import xrange from django.utils import timezone from django.utils import tree + # Connection types AND = 'AND' OR = 'OR' + class EmptyShortCircuit(Exception): """ Internal exception used to indicate that a "matches nothing" node should be @@ -27,6 +29,7 @@ class EmptyShortCircuit(Exception): """ pass + class WhereNode(tree.Node): """ Used to represent the SQL where-clause. @@ -58,7 +61,7 @@ class WhereNode(tree.Node): if not isinstance(data, (list, tuple)): return data obj, lookup_type, value = data - if is_iterator(value): + if isinstance(value, collections.Iterator): # Consume any generators immediately, so that we can determine # emptiness and transform any non-empty values correctly. value = list(value) @@ -175,7 +178,7 @@ class WhereNode(tree.Node): """ lvalue, lookup_type, value_annotation, params_or_value = child field_internal_type = lvalue.field.get_internal_type() if lvalue.field else None - + if isinstance(lvalue, Constraint): try: lvalue, params = lvalue.process(lookup_type, params_or_value, connection) @@ -304,14 +307,15 @@ class WhereNode(tree.Node): clone.children.append(child) return clone -class EmptyWhere(WhereNode): +class EmptyWhere(WhereNode): def add(self, data, connector): return def as_sql(self, qn=None, connection=None): raise EmptyResultSet + class EverythingNode(object): """ A node that matches everything. @@ -385,6 +389,7 @@ class Constraint(object): new.alias, new.col, new.field = change_map[self.alias], self.col, self.field return new + class SubqueryConstraint(object): def __init__(self, alias, columns, targets, query_object): self.alias = alias diff --git a/django/db/transaction.py b/django/db/transaction.py index 96be981e7b..bb2dbb8e9e 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -27,6 +27,7 @@ class TransactionManagementError(Exception): """ pass + ################ # Private APIs # ################ @@ -40,6 +41,7 @@ def get_connection(using=None): using = DEFAULT_DB_ALIAS return connections[using] + ########################### # Deprecated private APIs # ########################### @@ -56,6 +58,7 @@ def abort(using=None): """ get_connection(using).abort() + def enter_transaction_management(managed=True, using=None, forced=False): """ Enters transaction management for a running thread. It must be balanced with @@ -68,6 +71,7 @@ def enter_transaction_management(managed=True, using=None, forced=False): """ get_connection(using).enter_transaction_management(managed, forced) + def leave_transaction_management(using=None): """ Leaves transaction management for a running thread. A dirty flag is carried @@ -76,6 +80,7 @@ def leave_transaction_management(using=None): """ get_connection(using).leave_transaction_management() + def is_dirty(using=None): """ Returns True if the current transaction requires a commit for changes to @@ -83,6 +88,7 @@ def is_dirty(using=None): """ return get_connection(using).is_dirty() + def set_dirty(using=None): """ Sets a dirty flag for the current thread and code streak. This can be used @@ -91,6 +97,7 @@ def set_dirty(using=None): """ get_connection(using).set_dirty() + def set_clean(using=None): """ Resets a dirty flag for the current thread and code streak. This can be used @@ -99,22 +106,27 @@ def set_clean(using=None): """ get_connection(using).set_clean() + def is_managed(using=None): warnings.warn("'is_managed' is deprecated.", DeprecationWarning, stacklevel=2) + def managed(flag=True, using=None): warnings.warn("'managed' no longer serves a purpose.", DeprecationWarning, stacklevel=2) + def commit_unless_managed(using=None): warnings.warn("'commit_unless_managed' is now a no-op.", DeprecationWarning, stacklevel=2) + def rollback_unless_managed(using=None): warnings.warn("'rollback_unless_managed' is now a no-op.", DeprecationWarning, stacklevel=2) + ############### # Public APIs # ############### @@ -125,24 +137,28 @@ def get_autocommit(using=None): """ return get_connection(using).get_autocommit() + def set_autocommit(autocommit, using=None): """ Set the autocommit status of the connection. """ return get_connection(using).set_autocommit(autocommit) + def commit(using=None): """ Commits a transaction and resets the dirty flag. """ get_connection(using).commit() + def rollback(using=None): """ Rolls back a transaction and resets the dirty flag. """ get_connection(using).rollback() + def savepoint(using=None): """ Creates a savepoint (if supported and required by the backend) inside the @@ -151,6 +167,7 @@ def savepoint(using=None): """ return get_connection(using).savepoint() + def savepoint_rollback(sid, using=None): """ Rolls back the most recent savepoint (if one exists). Does nothing if @@ -158,6 +175,7 @@ def savepoint_rollback(sid, using=None): """ get_connection(using).savepoint_rollback(sid) + def savepoint_commit(sid, using=None): """ Commits the most recent savepoint (if one exists). Does nothing if @@ -165,18 +183,21 @@ def savepoint_commit(sid, using=None): """ get_connection(using).savepoint_commit(sid) + def clean_savepoints(using=None): """ Resets the counter used to generate unique savepoint ids in this thread. """ get_connection(using).clean_savepoints() + def get_rollback(using=None): """ Gets the "needs rollback" flag -- for *advanced use* only. """ return get_connection(using).get_rollback() + def set_rollback(rollback, using=None): """ Sets or unsets the "needs rollback" flag -- for *advanced use* only. @@ -191,6 +212,7 @@ def set_rollback(rollback, using=None): """ return get_connection(using).set_rollback(rollback) + ################################# # Decorators / context managers # ################################# @@ -398,6 +420,7 @@ class Transaction(object): return func(*args, **kwargs) return inner + def _transaction_func(entering, exiting, using): """ Takes 3 things, an entering function (what to do to start this block of @@ -436,6 +459,7 @@ def autocommit(using=None): return _transaction_func(entering, exiting, using) + def commit_on_success(using=None): """ This decorator activates commit on response. This way, if the view function @@ -466,6 +490,7 @@ def commit_on_success(using=None): return _transaction_func(entering, exiting, using) + def commit_manually(using=None): """ Decorator that activates manual transaction control. It just disables @@ -484,6 +509,7 @@ def commit_manually(using=None): return _transaction_func(entering, exiting, using) + def commit_on_success_unless_managed(using=None, savepoint=False): """ Transitory API to preserve backwards-compatibility while refactoring. diff --git a/django/forms/fields.py b/django/forms/fields.py index c4bc3fa88c..a794c02e9f 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -370,8 +370,14 @@ class DecimalField(IntegerField): def widget_attrs(self, widget): attrs = super(DecimalField, self).widget_attrs(widget) - if isinstance(widget, NumberInput) and self.decimal_places: - attrs['step'] = '0.%s1' % ('0' * (self.decimal_places - 1)) + if isinstance(widget, NumberInput): + if self.decimal_places is not None: + # Use exponential notation for small values since they might + # be parsed as 0 otherwise. ref #20765 + step = str(Decimal('1') / 10 ** self.decimal_places).lower() + else: + step = 'any' + attrs.setdefault('step', step) return attrs @@ -1019,7 +1025,7 @@ class MultiValueField(Field): if not isinstance(initial, list): initial = self.widget.decompress(initial) for field, initial, data in zip(self.fields, initial, data): - if field._has_changed(initial, data): + if field._has_changed(field.to_python(initial), data): return True return False diff --git a/django/forms/forms.py b/django/forms/forms.py index 6d59271d17..e144eb60f8 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -77,7 +77,7 @@ class BaseForm(object): # information. Any improvements to the form API should be made to *this* # class, not to the Form class. def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList, label_suffix=':', + initial=None, error_class=ErrorList, label_suffix=None, empty_permitted=False): self.is_bound = data is not None or files is not None self.data = data or {} @@ -86,7 +86,8 @@ class BaseForm(object): self.prefix = prefix self.initial = initial or {} self.error_class = error_class - self.label_suffix = label_suffix + # Translators: This is the default suffix added to form field labels + self.label_suffix = label_suffix if label_suffix is not None else _(':') self.empty_permitted = empty_permitted self._errors = None # Stores the errors after clean() has been called. self._changed_data = None @@ -518,7 +519,9 @@ class BoundField(object): """ contents = contents or self.label # Only add the suffix if the label does not end in punctuation. - if self.form.label_suffix and contents and contents[-1] not in ':?.!': + # Translators: If found as last label character, these punctuation + # characters will prevent the default label_suffix to be appended to the label + if self.form.label_suffix and contents and contents[-1] not in _(':?.!'): contents = format_html('{0}{1}', contents, self.form.label_suffix) widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id diff --git a/django/forms/models.py b/django/forms/models.py index a0c3009d38..1d3bec1635 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -292,7 +292,7 @@ class ModelFormMetaclass(type): class BaseModelForm(BaseForm): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList, label_suffix=':', + initial=None, error_class=ErrorList, label_suffix=None, empty_permitted=False, instance=None): opts = self._meta if opts.model is None: diff --git a/django/forms/util.py b/django/forms/util.py index 0a73320f83..3c7724900a 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import warnings + from django.conf import settings from django.utils.html import format_html, format_html_join from django.utils.encoding import force_text, python_2_unicode_compatible @@ -21,6 +23,11 @@ def flatatt(attrs): The result is passed through 'mark_safe'. """ + if [v for v in attrs.values() if v is True or v is False]: + warnings.warn( + 'The meaning of boolean values for widget attributes will change in Django 1.8', + DeprecationWarning + ) return format_html_join('', ' {0}="{1}"', sorted(attrs.items())) @python_2_unicode_compatible diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index eeb435fa57..9a5193e49b 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -50,7 +50,7 @@ class MultiPartParser(object): The raw post data, as a file-like object. :upload_handlers: A list of UploadHandler instances that perform operations on the uploaded - data. + data. :encoding: The encoding with which to treat the incoming data. """ @@ -177,11 +177,9 @@ class MultiPartParser(object): file_name = force_text(file_name, encoding, errors='replace') file_name = self.IE_sanitize(unescape_entities(file_name)) - content_type = meta_data.get('content-type', ('',))[0].strip() - try: - charset = meta_data.get('content-type', (0, {}))[1].get('charset', None) - except: - charset = None + content_type, content_type_extra = meta_data.get('content-type', ('', {})) + content_type = content_type.strip() + charset = content_type_extra.get('charset') try: content_length = int(meta_data.get('content-length')[0]) @@ -194,7 +192,7 @@ class MultiPartParser(object): try: handler.new_file(field_name, file_name, content_type, content_length, - charset) + charset, content_type_extra) except StopFutureHandlers: break diff --git a/django/template/context.py b/django/template/context.py index 1ef7e889bc..3830c1c660 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -12,6 +12,21 @@ class ContextPopException(Exception): "pop() has been called more times than push()" pass + +class ContextDict(dict): + def __init__(self, context, *args, **kwargs): + super(ContextDict, self).__init__(*args, **kwargs) + + context.dicts.append(self) + self.context = context + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.context.pop() + + class BaseContext(object): def __init__(self, dict_=None): self._reset_dicts(dict_) @@ -34,10 +49,8 @@ class BaseContext(object): for d in reversed(self.dicts): yield d - def push(self): - d = {} - self.dicts.append(d) - return d + def push(self, *args, **kwargs): + return ContextDict(self, *args, **kwargs) def pop(self): if len(self.dicts) == 1: @@ -83,6 +96,7 @@ class BaseContext(object): new_context._reset_dicts(values) return new_context + class Context(BaseContext): "A stack container for variable context" def __init__(self, dict_=None, autoescape=True, current_app=None, @@ -106,6 +120,7 @@ class Context(BaseContext): self.dicts.append(other_dict) return other_dict + class RenderContext(BaseContext): """ A stack container for storing Template state. diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 5b2a1b9501..5c9490f749 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -95,10 +95,9 @@ class FilterNode(Node): def render(self, context): output = self.nodelist.render(context) # Apply filters. - context.update({'var': output}) - filtered = self.filter_expr.resolve(context) - context.pop() - return filtered + with context.push(var=output): + return self.filter_expr.resolve(context) + class FirstOfNode(Node): def __init__(self, variables, escape=False): @@ -143,71 +142,69 @@ class ForNode(Node): parentloop = context['forloop'] else: parentloop = {} - context.push() - try: - values = self.sequence.resolve(context, True) - except VariableDoesNotExist: - values = [] - if values is None: - values = [] - if not hasattr(values, '__len__'): - values = list(values) - len_values = len(values) - if len_values < 1: - context.pop() - return self.nodelist_empty.render(context) - nodelist = NodeList() - if self.is_reversed: - values = reversed(values) - unpack = len(self.loopvars) > 1 - # Create a forloop value in the context. We'll update counters on each - # iteration just below. - loop_dict = context['forloop'] = {'parentloop': parentloop} - for i, item in enumerate(values): - # Shortcuts for current loop iteration number. - loop_dict['counter0'] = i - loop_dict['counter'] = i+1 - # Reverse counter iteration numbers. - loop_dict['revcounter'] = len_values - i - loop_dict['revcounter0'] = len_values - i - 1 - # Boolean values designating first and last times through loop. - loop_dict['first'] = (i == 0) - loop_dict['last'] = (i == len_values - 1) + with context.push(): + try: + values = self.sequence.resolve(context, True) + except VariableDoesNotExist: + values = [] + if values is None: + values = [] + if not hasattr(values, '__len__'): + values = list(values) + len_values = len(values) + if len_values < 1: + return self.nodelist_empty.render(context) + nodelist = NodeList() + if self.is_reversed: + values = reversed(values) + unpack = len(self.loopvars) > 1 + # Create a forloop value in the context. We'll update counters on each + # iteration just below. + loop_dict = context['forloop'] = {'parentloop': parentloop} + for i, item in enumerate(values): + # Shortcuts for current loop iteration number. + loop_dict['counter0'] = i + loop_dict['counter'] = i+1 + # Reverse counter iteration numbers. + loop_dict['revcounter'] = len_values - i + loop_dict['revcounter0'] = len_values - i - 1 + # Boolean values designating first and last times through loop. + loop_dict['first'] = (i == 0) + loop_dict['last'] = (i == len_values - 1) - pop_context = False - if unpack: - # If there are multiple loop variables, unpack the item into - # them. - try: - unpacked_vars = dict(zip(self.loopvars, item)) - except TypeError: - pass - else: - pop_context = True - context.update(unpacked_vars) - else: - context[self.loopvars[0]] = item - # In TEMPLATE_DEBUG mode provide source of the node which - # actually raised the exception - if settings.TEMPLATE_DEBUG: - for node in self.nodelist_loop: + pop_context = False + if unpack: + # If there are multiple loop variables, unpack the item into + # them. try: + unpacked_vars = dict(zip(self.loopvars, item)) + except TypeError: + pass + else: + pop_context = True + context.update(unpacked_vars) + else: + context[self.loopvars[0]] = item + # In TEMPLATE_DEBUG mode provide source of the node which + # actually raised the exception + if settings.TEMPLATE_DEBUG: + for node in self.nodelist_loop: + try: + nodelist.append(node.render(context)) + except Exception as e: + if not hasattr(e, 'django_template_source'): + e.django_template_source = node.source + raise + else: + for node in self.nodelist_loop: nodelist.append(node.render(context)) - except Exception as e: - if not hasattr(e, 'django_template_source'): - e.django_template_source = node.source - raise - else: - for node in self.nodelist_loop: - nodelist.append(node.render(context)) - if pop_context: - # The loop variables were pushed on to the context so pop them - # off again. This is necessary because the tag lets the length - # of loopvars differ to the length of each set of items and we - # don't want to leave any vars from the previous loop on the - # context. - context.pop() - context.pop() + if pop_context: + # The loop variables were pushed on to the context so pop them + # off again. This is necessary because the tag lets the length + # of loopvars differ to the length of each set of items and we + # don't want to leave any vars from the previous loop on the + # context. + context.pop() return nodelist.render(context) class IfChangedNode(Node): @@ -500,10 +497,9 @@ class WithNode(Node): def render(self, context): values = dict([(key, val.resolve(context)) for key, val in six.iteritems(self.extra_context)]) - context.update(values) - output = self.nodelist.render(context) - context.pop() - return output + with context.push(**values): + return self.nodelist.render(context) + @register.tag def autoescape(parser, token): diff --git a/django/template/loader.py b/django/template/loader.py index 6df4e43c4f..44b8f600fb 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -164,11 +164,8 @@ def render_to_string(template_name, dictionary=None, context_instance=None): return t.render(Context(dictionary)) # Add the dictionary to the context stack, ensuring it gets removed again # to keep the context_instance in the same state it started in. - context_instance.update(dictionary) - try: + with context_instance.push(dictionary): return t.render(context_instance) - finally: - context_instance.pop() def select_template(template_name_list): "Given a list of template names, returns the first that can be loaded." diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index 767f0e5ff8..406775da9d 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -47,22 +47,21 @@ class BlockNode(Node): def render(self, context): block_context = context.render_context.get(BLOCK_CONTEXT_KEY) - context.push() - if block_context is None: - context['block'] = self - result = self.nodelist.render(context) - else: - push = block = block_context.pop(self.name) - if block is None: - block = self - # Create new block so we can store context without thread-safety issues. - block = BlockNode(block.name, block.nodelist) - block.context = context - context['block'] = block - result = block.nodelist.render(context) - if push is not None: - block_context.push(self.name, push) - context.pop() + with context.push(): + if block_context is None: + context['block'] = self + result = self.nodelist.render(context) + else: + push = block = block_context.pop(self.name) + if block is None: + block = self + # Create new block so we can store context without thread-safety issues. + block = BlockNode(block.name, block.nodelist) + block.context = context + context['block'] = block + result = block.nodelist.render(context) + if push is not None: + block_context.push(self.name, push) return result def super(self): @@ -133,10 +132,9 @@ class BaseIncludeNode(Node): in six.iteritems(self.extra_context)]) if self.isolated_context: return template.render(context.new(values)) - context.update(values) - output = template.render(context) - context.pop() - return output + with context.push(**values): + return template.render(context) + class ConstantIncludeNode(BaseIncludeNode): def __init__(self, template_path, *args, **kwargs): diff --git a/django/test/client.py b/django/test/client.py index 94cfada725..8dbd33ccf1 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -180,7 +180,11 @@ def encode_multipart(boundary, data): def encode_file(boundary, key, file): to_bytes = lambda s: force_bytes(s, settings.DEFAULT_CHARSET) - content_type = mimetypes.guess_type(file.name)[0] + if hasattr(file, 'content_type'): + content_type = file.content_type + else: + content_type = mimetypes.guess_type(file.name)[0] + if content_type is None: content_type = 'application/octet-stream' return [ diff --git a/django/test/runner.py b/django/test/runner.py index 6897906098..c7881ada63 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -238,6 +238,7 @@ def setup_databases(verbosity, interactive, **kwargs): mirrored_aliases = {} test_databases = {} dependencies = {} + default_sig = connections[DEFAULT_DB_ALIAS].creation.test_db_signature() for alias in connections: connection = connections[alias] if connection.settings_dict['TEST_MIRROR']: @@ -259,7 +260,7 @@ def setup_databases(verbosity, interactive, **kwargs): dependencies[alias] = ( connection.settings_dict['TEST_DEPENDENCIES']) else: - if alias != DEFAULT_DB_ALIAS: + if alias != DEFAULT_DB_ALIAS and connection.creation.test_db_signature() != default_sig: dependencies[alias] = connection.settings_dict.get( 'TEST_DEPENDENCIES', [DEFAULT_DB_ALIAS]) @@ -271,15 +272,16 @@ def setup_databases(verbosity, interactive, **kwargs): test_databases.items(), dependencies): test_db_name = None # Actually create the database for the first connection - for alias in aliases: connection = connections[alias] - old_names.append((connection, db_name, True)) if test_db_name is None: test_db_name = connection.creation.create_test_db( verbosity, autoclobber=not interactive) + destroy = True else: connection.settings_dict['NAME'] = test_db_name + destroy = False + old_names.append((connection, db_name, destroy)) for alias, mirror_alias in mirrored_aliases.items(): mirrors.append((alias, connections[alias].settings_dict['NAME'])) diff --git a/django/test/signals.py b/django/test/signals.py index a96bdff3b3..de8802b381 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -1,5 +1,6 @@ import os import time +import warnings from django.conf import settings from django.db import connections @@ -9,11 +10,14 @@ from django.utils.functional import empty template_rendered = Signal(providing_args=["template", "context"]) -setting_changed = Signal(providing_args=["setting", "value"]) +setting_changed = Signal(providing_args=["setting", "value", "enter"]) # Most setting_changed receivers are supposed to be added below, # except for cases where the receiver is related to a contrib app. +# Settings that may not work well when using 'override_settings' (#19031) +COMPLEX_OVERRIDE_SETTINGS = set(['DATABASES']) + @receiver(setting_changed) def update_connections_time_zone(**kwargs): @@ -74,8 +78,15 @@ def language_changed(**kwargs): if kwargs['setting'] == 'LOCALE_PATHS': trans_real._translations = {} + @receiver(setting_changed) def file_storage_changed(**kwargs): if kwargs['setting'] in ('MEDIA_ROOT', 'DEFAULT_FILE_STORAGE'): from django.core.files.storage import default_storage default_storage._wrapped = empty + + +@receiver(setting_changed) +def complex_setting_changed(**kwargs): + if kwargs['enter'] and kwargs['setting'] in COMPLEX_OVERRIDE_SETTINGS: + warnings.warn("Overriding setting %s can lead to unexpected behaviour." % kwargs['setting']) diff --git a/django/test/testcases.py b/django/test/testcases.py index c5299bdabc..6f3f1c00e4 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -8,7 +8,6 @@ import json import os import re import sys -import select import socket import threading import unittest @@ -880,10 +879,19 @@ class TestCase(TransactionTestCase): self.atomics[db_name].__exit__(None, None, None) +class CheckCondition(object): + """Descriptor class for deferred condition checking""" + def __init__(self, cond_func): + self.cond_func = cond_func + + def __get__(self, obj, objtype): + return self.cond_func() + + def _deferredSkip(condition, reason): def decorator(test_func): if not (isinstance(test_func, type) and - issubclass(test_func, TestCase)): + issubclass(test_func, unittest.TestCase)): @wraps(test_func) def skip_wrapper(*args, **kwargs): if condition(): @@ -891,7 +899,9 @@ def _deferredSkip(condition, reason): return test_func(*args, **kwargs) test_item = skip_wrapper else: + # Assume a class is decorated test_item = test_func + test_item.__unittest_skip__ = CheckCondition(condition) test_item.__unittest_skip_why__ = reason return test_item return decorator @@ -924,104 +934,6 @@ class QuietWSGIRequestHandler(WSGIRequestHandler): pass -if sys.version_info >= (3, 3, 0): - _ImprovedEvent = threading.Event -elif sys.version_info >= (2, 7, 0): - _ImprovedEvent = threading._Event -else: - class _ImprovedEvent(threading._Event): - """ - Does the same as `threading.Event` except it overrides the wait() method - with some code borrowed from Python 2.7 to return the set state of the - event (see: http://hg.python.org/cpython/rev/b5aa8aa78c0f/). This allows - to know whether the wait() method exited normally or because of the - timeout. This class can be removed when Django supports only Python >= 2.7. - """ - - def wait(self, timeout=None): - self._Event__cond.acquire() - try: - if not self._Event__flag: - self._Event__cond.wait(timeout) - return self._Event__flag - finally: - self._Event__cond.release() - - -class StoppableWSGIServer(WSGIServer): - """ - The code in this class is borrowed from the `SocketServer.BaseServer` class - in Python 2.6. The important functionality here is that the server is non- - blocking and that it can be shut down at any moment. This is made possible - by the server regularly polling the socket and checking if it has been - asked to stop. - Note for the future: Once Django stops supporting Python 2.6, this class - can be removed as `WSGIServer` will have this ability to shutdown on - demand and will not require the use of the _ImprovedEvent class whose code - is borrowed from Python 2.7. - """ - - def __init__(self, *args, **kwargs): - super(StoppableWSGIServer, self).__init__(*args, **kwargs) - self.__is_shut_down = _ImprovedEvent() - self.__serving = False - - def serve_forever(self, poll_interval=0.5): - """ - Handle one request at a time until shutdown. - - Polls for shutdown every poll_interval seconds. - """ - self.__serving = True - self.__is_shut_down.clear() - while self.__serving: - r, w, e = select.select([self], [], [], poll_interval) - if r: - self._handle_request_noblock() - self.__is_shut_down.set() - - def shutdown(self): - """ - Stops the serve_forever loop. - - Blocks until the loop has finished. This must be called while - serve_forever() is running in another thread, or it will - deadlock. - """ - self.__serving = False - if not self.__is_shut_down.wait(2): - raise RuntimeError( - "Failed to shutdown the live test server in 2 seconds. The " - "server might be stuck or generating a slow response.") - - def handle_request(self): - """Handle one request, possibly blocking. - """ - fd_sets = select.select([self], [], [], None) - if not fd_sets[0]: - return - self._handle_request_noblock() - - def _handle_request_noblock(self): - """ - Handle one request, without blocking. - - I assume that select.select has returned that the socket is - readable before this function was called, so there should be - no risk of blocking in get_request(). - """ - try: - request, client_address = self.get_request() - except socket.error: - return - if self.verify_request(request, client_address): - try: - self.process_request(request, client_address) - except Exception: - self.handle_error(request, client_address) - self.close_request(request) - - class _MediaFilesHandler(StaticFilesHandler): """ Handler for serving the media files. This is a private class that is @@ -1071,7 +983,7 @@ class LiveServerThread(threading.Thread): # one that is free to use for the WSGI server. for index, port in enumerate(self.possible_ports): try: - self.httpd = StoppableWSGIServer( + self.httpd = WSGIServer( (self.host, port), QuietWSGIRequestHandler) except WSGIServerException as e: if (index + 1 < len(self.possible_ports) and @@ -1128,7 +1040,7 @@ class LiveServerTestCase(TransactionTestCase): for conn in connections.all(): # If using in-memory sqlite databases, pass the connections to # the server thread. - if (conn.settings_dict['ENGINE'].rsplit('.', 1)[-1] in ('sqlite3', 'spatialite') + if (conn.vendor == 'sqlite' and conn.settings_dict['NAME'] == ':memory:'): # Explicitly enable thread-shareability for this connection conn.allow_thread_sharing = True @@ -1180,7 +1092,7 @@ class LiveServerTestCase(TransactionTestCase): # Restore sqlite connections' non-sharability for conn in connections.all(): - if (conn.settings_dict['ENGINE'].rsplit('.', 1)[-1] in ('sqlite3', 'spatialite') + if (conn.vendor == 'sqlite' and conn.settings_dict['NAME'] == ':memory:'): conn.allow_thread_sharing = False diff --git a/django/test/utils.py b/django/test/utils.py index 70ce640239..dc3e5952ea 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -228,7 +228,7 @@ class override_settings(object): settings._wrapped = override for key, new_value in self.options.items(): setting_changed.send(sender=settings._wrapped.__class__, - setting=key, value=new_value) + setting=key, value=new_value, enter=True) def disable(self): settings._wrapped = self.wrapped @@ -236,7 +236,7 @@ class override_settings(object): for key in self.options: new_value = getattr(settings, key, None) setting_changed.send(sender=settings._wrapped.__class__, - setting=key, value=new_value) + setting=key, value=new_value, enter=False) def compare_xml(want, got): diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index 7eba842a89..de32b9f8cc 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -114,11 +114,11 @@ class SyndicationFeed(object): def add_item(self, title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, unique_id_is_permalink=None, enclosure=None, - categories=(), item_copyright=None, ttl=None, **kwargs): + categories=(), item_copyright=None, ttl=None, updateddate=None, **kwargs): """ Adds an item to the feed. All args are expected to be Python Unicode - objects except pubdate, which is a datetime.datetime object, and - enclosure, which is an instance of the Enclosure class. + objects except pubdate and updateddate, which are datetime.datetime + objects, and enclosure, which is an instance of the Enclosure class. """ to_unicode = lambda s: force_text(s, strings_only=True) if categories: @@ -134,6 +134,7 @@ class SyndicationFeed(object): 'author_name': to_unicode(author_name), 'author_link': iri_to_uri(author_link), 'pubdate': pubdate, + 'updateddate': updateddate, 'comments': to_unicode(comments), 'unique_id': to_unicode(unique_id), 'unique_id_is_permalink': unique_id_is_permalink, @@ -191,15 +192,20 @@ class SyndicationFeed(object): def latest_post_date(self): """ - Returns the latest item's pubdate. If none of them have a pubdate, - this returns the current date/time. + Returns the latest item's pubdate or updateddate. If no items + have either of these attributes this returns the current date/time. """ - updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None] - if len(updates) > 0: - updates.sort() - return updates[-1] - else: - return datetime.datetime.now() + latest_date = None + date_keys = ('updateddate', 'pubdate') + + for item in self.items: + for date_key in date_keys: + item_date = item.get(date_key) + if item_date: + if latest_date is None or item_date > latest_date: + latest_date = item_date + + return latest_date or datetime.datetime.now() class Enclosure(object): "Represents an RSS enclosure" @@ -349,8 +355,12 @@ class Atom1Feed(SyndicationFeed): def add_item_elements(self, handler, item): handler.addQuickElement("title", item['title']) handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"}) + if item['pubdate'] is not None: - handler.addQuickElement("updated", rfc3339_date(item['pubdate'])) + handler.addQuickElement('published', rfc3339_date(item['pubdate'])) + + if item['updateddate'] is not None: + handler.addQuickElement('updated', rfc3339_date(item['updateddate'])) # Author information. if item['author_name'] is not None: diff --git a/django/utils/functional.py b/django/utils/functional.py index 0606c775ef..e23bd3ff80 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -403,9 +403,8 @@ def partition(predicate, values): if sys.version_info >= (2, 7, 2): from functools import total_ordering else: - # For Python < 2.7.2. Python 2.6 does not have total_ordering, and - # total_ordering in 2.7 versions prior to 2.7.2 is buggy. See - # http://bugs.python.org/issue10042 for details. For these versions use + # For Python < 2.7.2. total_ordering in versions prior to 2.7.2 is buggy. + # See http://bugs.python.org/issue10042 for details. For these versions use # code borrowed from Python 2.7.3. def total_ordering(cls): """Class decorator that fills in missing ordering methods""" diff --git a/django/utils/image.py b/django/utils/image.py index 2fd0c6e6b0..dd2fab6197 100644 --- a/django/utils/image.py +++ b/django/utils/image.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ To provide a shim layer over Pillow/PIL situation until the PIL support is -removed. +removed. See #19934. Combinations To Account For @@ -132,6 +132,8 @@ def _detect_image_library(): try: from PIL import ImageFile as PILImageFile except ImportError: + # This import cannot fail unless Pillow/PIL install is completely + # broken (e.g. missing Python modules). import ImageFile as PILImageFile # Finally, warn about deprecation... diff --git a/django/utils/itercompat.py b/django/utils/itercompat.py index 1df1e57cb2..096f808671 100644 --- a/django/utils/itercompat.py +++ b/django/utils/itercompat.py @@ -4,9 +4,6 @@ Where possible, we try to use the system-native version and only fall back to these implementations if necessary. """ -import collections -import sys - def is_iterable(x): "A implementation independent way of checking for iterables" @@ -16,14 +13,3 @@ def is_iterable(x): return False else: return True - -def is_iterator(x): - """An implementation independent way of checking for iterators - - Python 2.6 has a different implementation of collections.Iterator which - accepts anything with a `next` method. 2.7+ requires and `__iter__` method - as well. - """ - if sys.version_info >= (2, 7): - return isinstance(x, collections.Iterator) - return isinstance(x, collections.Iterator) and hasattr(x, '__iter__') diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 46c387f262..8fb0f64e09 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -17,7 +17,8 @@ def timesince(d, now=None, reversed=False): displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. - Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since + Adapted from + http://web.archive.org/web/20060617175230/http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ chunks = ( (60 * 60 * 24 * 365, ungettext_lazy('%d year', '%d years')), |
