diff --git a/AUTHORS b/AUTHORS index 4ba21fe0d9..ae34fbecae 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,8 @@ The PRIMARY AUTHORS are (and/or have been): * Brian Rosner * Justin Bronn * Karen Tracey + * Jannis Leidel + * James Tauber More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -26,6 +28,7 @@ answer newbie questions, and generally made Django that much better: ajs alang@bright-green.com + Andi Albrecht Marty Alchin Ahmad Alhashemi Daniel Alves Barbosa de Oliveira Vaz @@ -267,7 +270,6 @@ answer newbie questions, and generally made Django that much better: lcordier@point45.com Jeong-Min Lee Tai Lee - Jannis Leidel Christopher Lenz lerouxb@gmail.com Piotr Lewandowski @@ -422,7 +424,7 @@ answer newbie questions, and generally made Django that much better: Travis Terry thebjorn Zach Thompson - Michael Thornhill + Michael Thornhill Deepak Thukral tibimicu@gmx.net tobias@neuyork.de @@ -470,6 +472,8 @@ answer newbie questions, and generally made Django that much better: Gasper Zejn Jarek Zgoda Cheng Zhang + Glenn Maynard + bthomas A big THANK YOU goes to: diff --git a/INSTALL b/INSTALL index 9b79ec016b..644c524bbb 100644 --- a/INSTALL +++ b/INSTALL @@ -1,22 +1,16 @@ Thanks for downloading Django. -To install it, make sure you have Python 2.3 or greater installed. Then run +To install it, make sure you have Python 2.4 or greater installed. Then run this command from the command prompt: python setup.py install -Note this requires a working Internet connection if you don't already have the -Python utility "setuptools" installed. - AS AN ALTERNATIVE, you can just copy the entire "django" directory to Python's site-packages directory, which is located wherever your Python installation lives. Some places you might check are: + /usr/lib/python2.5/site-packages (Unix, Python 2.5) /usr/lib/python2.4/site-packages (Unix, Python 2.4) - /usr/lib/python2.3/site-packages (Unix, Python 2.3) C:\\PYTHON\site-packages (Windows) -This second solution does not require a working Internet connection; it -bypasses "setuptools" entirely. - For more detailed instructions, see docs/intro/install.txt. diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 7bc7ae9508..7fbfd26534 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -108,9 +108,6 @@ class Settings(object): os.environ['TZ'] = self.TIME_ZONE time.tzset() - def get_all_members(self): - return dir(self) - class UserSettingsHolder(object): """ Holder for user configured settings. @@ -129,8 +126,11 @@ class UserSettingsHolder(object): def __getattr__(self, name): return getattr(self.default_settings, name) - def get_all_members(self): + def __dir__(self): return dir(self) + dir(self.default_settings) + # For Python < 2.6: + __members__ = property(lambda self: self.__dir__()) + settings = LazySettings() diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 7867accab9..0f96eadf45 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -134,6 +134,12 @@ DATABASE_OPTIONS = {} # Set to empty dictionary for default. DATABASES = { } +# The email backend to use. For possible shortcuts see django.core.mail. +# The default is to use the SMTP backend. +# Third-party backends can be specified by providing a Python path +# to a module that defines an EmailBackend class. +EMAIL_BACKEND = 'django.core.mail.backends.smtp' + # Host for sending e-mail. EMAIL_HOST = 'localhost' @@ -303,6 +309,7 @@ DEFAULT_INDEX_TABLESPACE = '' MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 'django.middleware.http.ConditionalGetMiddleware', # 'django.middleware.gzip.GZipMiddleware', @@ -377,6 +384,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/' # The number of days a password reset link is valid for PASSWORD_RESET_TIMEOUT_DAYS = 3 +######## +# CSRF # +######## + +# Dotted path to callable to be used as view when a request is +# rejected by the CSRF middleware. +CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure' + +# Name and domain for CSRF cookie. +CSRF_COOKIE_NAME = 'csrftoken' +CSRF_COOKIE_DOMAIN = None + ########### # TESTING # ########### diff --git a/django/conf/locale/pl/LC_MESSAGES/django.po b/django/conf/locale/pl/LC_MESSAGES/django.po index ba985d01e9..28f12561ee 100644 --- a/django/conf/locale/pl/LC_MESSAGES/django.po +++ b/django/conf/locale/pl/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-07-17 21:59+0200\n" +"POT-Creation-Date: 2009-10-25 20:56+0100\n" "PO-Revision-Date: 2008-02-25 15:53+0100\n" "Last-Translator: Jarek Zgoda \n" "MIME-Version: 1.0\n" @@ -266,15 +266,15 @@ msgstr "Ten miesiąc" msgid "This year" msgstr "Ten rok" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:434 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:435 msgid "Yes" msgstr "Tak" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:434 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:435 msgid "No" msgstr "Nie" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:434 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:435 msgid "Unknown" msgstr "Nieznany" @@ -320,8 +320,8 @@ msgid "Changed %s." msgstr "Zmieniono %s" #: contrib/admin/options.py:519 contrib/admin/options.py:529 -#: contrib/comments/templates/comments/preview.html:16 forms/models.py:388 -#: forms/models.py:600 +#: contrib/comments/templates/comments/preview.html:16 forms/models.py:384 +#: forms/models.py:596 msgid "and" msgstr "i" @@ -417,11 +417,11 @@ msgstr "" "Proszę wpisać poprawną nazwę użytkownika i hasło. Uwaga: wielkość liter ma " "znaczenie." -#: contrib/admin/sites.py:285 contrib/admin/views/decorators.py:40 +#: contrib/admin/sites.py:288 contrib/admin/views/decorators.py:40 msgid "Please log in again, because your session has expired." msgstr "Twoja sesja wygasła, zaloguj się ponownie." -#: contrib/admin/sites.py:292 contrib/admin/views/decorators.py:47 +#: contrib/admin/sites.py:295 contrib/admin/views/decorators.py:47 msgid "" "Looks like your browser isn't configured to accept cookies. Please enable " "cookies, reload this page, and try again." @@ -429,27 +429,27 @@ msgstr "" "Twoja przeglądarka nie chce akceptować ciasteczek. Zmień jej ustawienia i " "spróbuj ponownie." -#: contrib/admin/sites.py:308 contrib/admin/sites.py:314 +#: contrib/admin/sites.py:311 contrib/admin/sites.py:317 #: contrib/admin/views/decorators.py:66 msgid "Usernames cannot contain the '@' character." msgstr "Nazwy użytkowników nie mogą zawierać znaku '@'." -#: contrib/admin/sites.py:311 contrib/admin/views/decorators.py:62 +#: contrib/admin/sites.py:314 contrib/admin/views/decorators.py:62 #, python-format msgid "Your e-mail address is not your username. Try '%s' instead." msgstr "Podany adres e-mail nie jest Twoją nazwą użytkownika. Spróbuj '%s'." -#: contrib/admin/sites.py:367 +#: contrib/admin/sites.py:370 msgid "Site administration" msgstr "Administracja stroną" -#: contrib/admin/sites.py:381 contrib/admin/templates/admin/login.html:26 +#: contrib/admin/sites.py:384 contrib/admin/templates/admin/login.html:26 #: contrib/admin/templates/registration/password_reset_complete.html:14 #: contrib/admin/views/decorators.py:20 msgid "Log in" msgstr "Zaloguj się" -#: contrib/admin/sites.py:426 +#: contrib/admin/sites.py:429 #, python-format msgid "%s administration" msgstr "%s - administracja" @@ -464,27 +464,27 @@ msgstr "Jedno lub więcej %(fieldname)s w %(name)s: %(obj)s" msgid "One or more %(fieldname)s in %(name)s:" msgstr "Jedno lub więcej %(fieldname)s w %(name)s:" -#: contrib/admin/widgets.py:71 +#: contrib/admin/widgets.py:72 msgid "Date:" msgstr "Data:" -#: contrib/admin/widgets.py:71 +#: contrib/admin/widgets.py:72 msgid "Time:" msgstr "Czas:" -#: contrib/admin/widgets.py:95 +#: contrib/admin/widgets.py:96 msgid "Currently:" msgstr "Teraz:" -#: contrib/admin/widgets.py:95 +#: contrib/admin/widgets.py:96 msgid "Change:" msgstr "Zmień:" -#: contrib/admin/widgets.py:124 +#: contrib/admin/widgets.py:125 msgid "Lookup" msgstr "Szukaj" -#: contrib/admin/widgets.py:235 +#: contrib/admin/widgets.py:237 msgid "Add Another" msgstr "Dodaj kolejny" @@ -598,7 +598,7 @@ msgstr "Historia" #: contrib/admin/templates/admin/change_form.html:28 #: contrib/admin/templates/admin/edit_inline/stacked.html:13 -#: contrib/admin/templates/admin/edit_inline/tabular.html:27 +#: contrib/admin/templates/admin/edit_inline/tabular.html:28 msgid "View on site" msgstr "Pokaż na stronie" @@ -668,10 +668,10 @@ msgstr "" #, python-format msgid "" "Are you sure you want to delete the selected %(object_name)s objects? All of " -"the following objects and it's related items will be deleted:" +"the following objects and their related items will be deleted:" msgstr "" -"Czy chcesz skasować %(object_name)s? Następujące obiekty i zależne od nich " -"zostaną skasowane:" +"Czy chcesz skasować wybrane %(object_name)s? Następujące obiekty i zależne od " +"nich zostaną skasowane:" #: contrib/admin/templates/admin/filter.html:2 #, python-format @@ -734,7 +734,6 @@ msgid "User" msgstr "Użytkownik" #: contrib/admin/templates/admin/object_history.html:24 -#: contrib/comments/templates/comments/moderation_queue.html:33 msgid "Action" msgstr "Akcja" @@ -1125,7 +1124,6 @@ msgid "Time" msgstr "Czas" #: contrib/admindocs/views.py:359 contrib/comments/forms.py:95 -#: contrib/comments/templates/comments/moderation_queue.html:37 #: contrib/flatpages/admin.py:8 contrib/flatpages/models.py:7 msgid "URL" msgstr "URL" @@ -1428,22 +1426,54 @@ msgstr "użytkownicy" msgid "message" msgstr "wiadomość" -#: contrib/auth/views.py:56 +#: contrib/auth/views.py:58 msgid "Logged out" msgstr "Wylogowany" -#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:429 +#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:428 msgid "Enter a valid e-mail address." msgstr "Wprowadź poprawny adres e-mail." -#: contrib/comments/admin.py:11 +#: contrib/comments/admin.py:12 msgid "Content" msgstr "Zawartość" -#: contrib/comments/admin.py:14 +#: contrib/comments/admin.py:15 msgid "Metadata" msgstr "Metadane" +#: contrib/comments/admin.py:39 +msgid "flagged" +msgstr "oflagowany" + +#: contrib/comments/admin.py:40 +msgid "Flag selected comments" +msgstr "Oflaguj wybrane komentarze" + +#: contrib/comments/admin.py:43 +msgid "approved" +msgstr "zaakceptowany" + +#: contrib/comments/admin.py:44 +msgid "Approve selected comments" +msgstr "Zaakceptuj wybrane komentarze" + +#: contrib/comments/admin.py:47 +msgid "removed" +msgstr "usunięty" + +#: contrib/comments/admin.py:48 +msgid "Remove selected comments" +msgstr "Usuń wybrane komentarze" + +#: contrib/comments/admin.py:60 +#, python-format +msgid "1 comment was successfully %(action)s." +msgid_plural "%(count)s comments were successfully %(action)s." +msgstr[0] "1 komentarz został %(action)s" +msgstr[1] "%(count)s komentarze zostały %(action)s" +msgstr[2] "%(count)s komentarzy zostało %(action)s" + #: contrib/comments/feeds.py:13 #, python-format msgid "%(site_name)s comments" @@ -1455,7 +1485,6 @@ msgid "Latest comments on %(site_name)s" msgstr "Ostatnie komentarze na %(site_name)s" #: contrib/comments/forms.py:93 -#: contrib/comments/templates/comments/moderation_queue.html:34 msgid "Name" msgstr "Nazwa" @@ -1464,7 +1493,6 @@ msgid "Email address" msgstr "Adres e-mail" #: contrib/comments/forms.py:96 -#: contrib/comments/templates/comments/moderation_queue.html:35 msgid "Comment" msgstr "Komentarz" @@ -1592,7 +1620,6 @@ msgid "Really make this comment public?" msgstr "Czy ten komentarz na pewno ma być publiczny?" #: contrib/comments/templates/comments/approve.html:12 -#: contrib/comments/templates/comments/moderation_queue.html:49 msgid "Approve" msgstr "Zaakceptuj" @@ -1618,7 +1645,6 @@ msgid "Really remove this comment?" msgstr "Czy na pewno usunąć ten komentarz?" #: contrib/comments/templates/comments/delete.html:12 -#: contrib/comments/templates/comments/moderation_queue.html:53 msgid "Remove" msgstr "Usuń" @@ -1652,39 +1678,6 @@ msgstr "Zapisz" msgid "Preview" msgstr "Podgląd" -#: contrib/comments/templates/comments/moderation_queue.html:4 -#: contrib/comments/templates/comments/moderation_queue.html:19 -msgid "Comment moderation queue" -msgstr "Kolejka moderacji komentarzy" - -#: contrib/comments/templates/comments/moderation_queue.html:26 -msgid "No comments to moderate" -msgstr "Żaden komentarz nie oczekuje na akceptację" - -#: contrib/comments/templates/comments/moderation_queue.html:36 -msgid "Email" -msgstr "E-mail" - -#: contrib/comments/templates/comments/moderation_queue.html:38 -msgid "Authenticated?" -msgstr "Zalogowany?" - -#: contrib/comments/templates/comments/moderation_queue.html:39 -msgid "IP Address" -msgstr "Adres IP" - -#: contrib/comments/templates/comments/moderation_queue.html:40 -msgid "Date posted" -msgstr "Data dodania" - -#: contrib/comments/templates/comments/moderation_queue.html:63 -msgid "yes" -msgstr "tak" - -#: contrib/comments/templates/comments/moderation_queue.html:63 -msgid "no" -msgstr "nie" - #: contrib/comments/templates/comments/posted.html:4 msgid "Thanks for commenting" msgstr "Dziękujemy za dodanie komentarza" @@ -2599,6 +2592,10 @@ msgstr "Niepoprawna suma kontrolna numeru konta bankowego." msgid "Enter a valid Finnish social security number." msgstr "Wpis poprawny numer fińskiego ubezpieczenia socjalnego." +#: contrib/localflavor/fr/forms.py:30 +msgid "Phone numbers must be in 0X XX XX XX XX format." +msgstr "Numery telefoniczne muszą być w formacie 0X XX XX XX XX." + #: contrib/localflavor/in_/forms.py:14 msgid "Enter a zip code in the format XXXXXXX." msgstr "Wpisz kod pocztowy w formacie XXXXXXX." @@ -3944,86 +3941,86 @@ msgstr[2] "" "Proszę podać poprawne identyfikatory %(self)s. Wartości %(value)r są " "niepoprawne." -#: forms/fields.py:54 +#: forms/fields.py:53 msgid "This field is required." msgstr "To pole jest wymagane." -#: forms/fields.py:55 +#: forms/fields.py:54 msgid "Enter a valid value." msgstr "Wpisz poprawną wartość." -#: forms/fields.py:138 +#: forms/fields.py:137 #, python-format msgid "Ensure this value has at most %(max)d characters (it has %(length)d)." msgstr "" "Upewnij się, że ta wartość ma co najwyżej %(max)d znaków (ma długość %" "(length)d)." -#: forms/fields.py:139 +#: forms/fields.py:138 #, python-format msgid "Ensure this value has at least %(min)d characters (it has %(length)d)." msgstr "" "Upewnij się, że ta wartość ma co najmniej %(min)d znaków (ma długość %" "(length)d)." -#: forms/fields.py:166 +#: forms/fields.py:165 msgid "Enter a whole number." msgstr "Wpisz liczbę całkowitą." -#: forms/fields.py:167 forms/fields.py:196 forms/fields.py:225 +#: forms/fields.py:166 forms/fields.py:195 forms/fields.py:224 #, python-format msgid "Ensure this value is less than or equal to %s." msgstr "Upewnij się, że ta wartość jest mniejsza lub równa %s." -#: forms/fields.py:168 forms/fields.py:197 forms/fields.py:226 +#: forms/fields.py:167 forms/fields.py:196 forms/fields.py:225 #, python-format msgid "Ensure this value is greater than or equal to %s." msgstr "Upewnij się, że ta wartość jest większa lub równa %s." -#: forms/fields.py:195 forms/fields.py:224 +#: forms/fields.py:194 forms/fields.py:223 msgid "Enter a number." msgstr "Wpisz liczbę." -#: forms/fields.py:227 +#: forms/fields.py:226 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Upewnij się, że jest nie więcej niż %s cyfr." -#: forms/fields.py:228 +#: forms/fields.py:227 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Upewnij się, że jest nie więcej niż %s miejsc po przecinku." -#: forms/fields.py:229 +#: forms/fields.py:228 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Upewnij się, że jest nie więcej niż %s miejsc przed przecinkiem." -#: forms/fields.py:288 forms/fields.py:863 +#: forms/fields.py:287 forms/fields.py:862 msgid "Enter a valid date." msgstr "Wpisz poprawną datę." -#: forms/fields.py:322 forms/fields.py:864 +#: forms/fields.py:321 forms/fields.py:863 msgid "Enter a valid time." msgstr "Wpisz poprawną godzinę." -#: forms/fields.py:361 +#: forms/fields.py:360 msgid "Enter a valid date/time." msgstr "Wpisz poprawną datę/godzinę." -#: forms/fields.py:447 +#: forms/fields.py:446 msgid "No file was submitted. Check the encoding type on the form." msgstr "Nie wysłano żadnego pliku. Sprawdź typ kodowania formularza." -#: forms/fields.py:448 +#: forms/fields.py:447 msgid "No file was submitted." msgstr "Żaden plik nie został przesłany." -#: forms/fields.py:449 +#: forms/fields.py:448 msgid "The submitted file is empty." msgstr "Wysłany plik jest pusty." -#: forms/fields.py:450 +#: forms/fields.py:449 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -4031,7 +4028,7 @@ msgstr "" "Upewnij się, że nazwa tego pliku ma co najwyżej %(max)d znaków (ma długość %" "(length)d)." -#: forms/fields.py:483 +#: forms/fields.py:482 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -4039,29 +4036,29 @@ msgstr "" "Wgraj poprawny plik graficzny. Ten, który został wgrany, nie jest obrazem, " "albo jest uszkodzony." -#: forms/fields.py:544 +#: forms/fields.py:543 msgid "Enter a valid URL." msgstr "Wpisz poprawny URL." -#: forms/fields.py:545 +#: forms/fields.py:544 msgid "This URL appears to be a broken link." msgstr "Ten odnośnik jest nieprawidłowy." -#: forms/fields.py:625 forms/fields.py:703 +#: forms/fields.py:624 forms/fields.py:702 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Wybierz poprawną wartość. %(value)s nie jest jednym z dostępnych wyborów." -#: forms/fields.py:704 forms/fields.py:765 forms/models.py:1003 +#: forms/fields.py:703 forms/fields.py:764 forms/models.py:999 msgid "Enter a list of values." msgstr "Podaj listę wartości." -#: forms/fields.py:892 +#: forms/fields.py:891 msgid "Enter a valid IPv4 address." msgstr "Wprowadź poprawny adres IPv4." -#: forms/fields.py:902 +#: forms/fields.py:901 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "To pole może zawierać jedynie litery, cyfry, podkreślenia i myślniki." @@ -4070,29 +4067,29 @@ msgstr "To pole może zawierać jedynie litery, cyfry, podkreślenia i myślniki msgid "Order" msgstr "Porządek" -#: forms/models.py:367 +#: forms/models.py:363 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" "Wartości w %(field_name)s muszą być unikalne dla wyszukiwań %(lookup)s w %" "(date_field)s" -#: forms/models.py:381 forms/models.py:389 +#: forms/models.py:377 forms/models.py:385 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "%(field_label)s już istnieje w %(model_name)s." -#: forms/models.py:594 +#: forms/models.py:590 #, python-format msgid "Please correct the duplicate data for %(field)s." msgstr "Popraw zduplikowane dane w %(field)s." -#: forms/models.py:598 +#: forms/models.py:594 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." msgstr "Popraw zduplikowane dane w %(field)s, które wymaga unikalności." -#: forms/models.py:604 +#: forms/models.py:600 #, python-format msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " @@ -4101,24 +4098,24 @@ msgstr "" "Popraw zduplikowane dane w %(field_name)s, które wymaga unikalności dla %" "(lookup)s w polu %(date_field)s." -#: forms/models.py:612 +#: forms/models.py:608 msgid "Please correct the duplicate values below." msgstr "Popraw poniższe zduplikowane wartości." -#: forms/models.py:867 +#: forms/models.py:863 msgid "The inline foreign key did not match the parent instance primary key." msgstr "Osadzony klucz obcy nie pasuje do klucza głównego obiektu rodzica." -#: forms/models.py:930 +#: forms/models.py:926 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "Wybierz poprawną wartość. Podana nie jest jednym z dostępnych wyborów." -#: forms/models.py:1004 +#: forms/models.py:1000 #, python-format msgid "Select a valid choice. %s is not one of the available choices." msgstr "Wybierz poprawną wartość. %s nie jest jednym z dostępnych wyborów." -#: forms/models.py:1006 +#: forms/models.py:1002 #, python-format msgid "\"%s\" is not a valid value for a primary key." msgstr "\"%s\" nie jest poprawną wartością klucza głównego." @@ -4444,3 +4441,27 @@ msgstr "%(verbose_name)s zostało pomyślnie zmienione." #, python-format msgid "The %(verbose_name)s was deleted." msgstr "%(verbose_name)s zostało usunięte." + +#~ msgid "Comment moderation queue" +#~ msgstr "Kolejka moderacji komentarzy" + +#~ msgid "No comments to moderate" +#~ msgstr "Żaden komentarz nie oczekuje na akceptację" + +#~ msgid "Email" +#~ msgstr "E-mail" + +#~ msgid "Authenticated?" +#~ msgstr "Zalogowany?" + +#~ msgid "IP Address" +#~ msgstr "Adres IP" + +#~ msgid "Date posted" +#~ msgstr "Data dodania" + +#~ msgid "yes" +#~ msgstr "tak" + +#~ msgid "no" +#~ msgstr "nie" diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index 57fc6b5f0c..7038bba56e 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -65,6 +65,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 5eb66b4d36..43033780ac 100644 --- a/django/contrib/admin/media/css/changelists.css +++ b/django/contrib/admin/media/css/changelists.css @@ -53,7 +53,7 @@ vertical-align: middle; } -#changelist table thead th:first-child { +#changelist table thead th.action-checkbox-column { width: 1.5em; text-align: center; } diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 3144a22a2a..0c663a1c80 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets from django.contrib.admin import helpers from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict +from django.views.decorators.csrf import csrf_protect from django.core.exceptions import PermissionDenied from django.db import models, transaction from django.db.models.fields import BLANK_CHOICE_DASH @@ -152,8 +153,9 @@ class BaseModelAdmin(object): """ Get a form Field for a ManyToManyField. """ - # If it uses an intermediary model, don't show field in admin. - if db_field.rel.through is not None: + # If it uses an intermediary model that isn't auto created, don't show + # a field in admin. + if not db_field.rel.through._meta.auto_created: return None if db_field.name in self.raw_id_fields: @@ -701,6 +703,8 @@ class ModelAdmin(BaseModelAdmin): else: return HttpResponseRedirect(".") + @csrf_protect + @transaction.commit_on_success def add_view(self, request, form_url='', extra_context=None): "The 'add' admin view for this model." model = self.model @@ -782,8 +786,9 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return self.render_change_form(request, context, form_url=form_url, add=True) - add_view = transaction.commit_on_success(add_view) + @csrf_protect + @transaction.commit_on_success def change_view(self, request, object_id, extra_context=None): "The 'change' admin view for this model." model = self.model @@ -871,8 +876,8 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return self.render_change_form(request, context, change=True, obj=obj) - change_view = transaction.commit_on_success(change_view) + @csrf_protect def changelist_view(self, request, extra_context=None): "The 'change list' admin view for this model." from django.contrib.admin.views.main import ChangeList, ERROR_FLAG @@ -985,6 +990,7 @@ class ModelAdmin(BaseModelAdmin): 'admin/change_list.html' ], context, context_instance=context_instance) + @csrf_protect def delete_view(self, request, object_id, extra_context=None): "The 'delete' admin view for this model." opts = self.model._meta diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 5f397ecb01..52ef57370d 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -3,6 +3,7 @@ from django import http, template from django.contrib.admin import ModelAdmin from django.contrib.admin import actions from django.contrib.auth import authenticate, login +from django.views.decorators.csrf import csrf_protect from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse @@ -186,11 +187,17 @@ class AdminSite(object): return view(request, *args, **kwargs) if not cacheable: inner = never_cache(inner) + # We add csrf_protect here so this function can be used as a utility + # function for any view, without having to repeat 'csrf_protect'. + inner = csrf_protect(inner) return update_wrapper(inner, view) def get_urls(self): from django.conf.urls.defaults import patterns, url, include + if settings.DEBUG: + self.check_dependencies() + def wrap(view, cacheable=False): def wrapper(*args, **kwargs): return self.admin_view(view, cacheable)(*args, **kwargs) diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index d28dd0f45c..11414d1465 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -15,7 +15,7 @@ {% endif %}{% endblock %} {% block content %}
-
{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if form.errors %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index f645d65a0f..c5ac729c7e 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -29,7 +29,7 @@ {% endif %}{% endif %} {% endblock %} -{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if save_on_top %}{% submit_row %}{% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 31bf7bd29a..20b2eff060 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -68,7 +68,7 @@ {% endif %} {% endblock %} - + {% csrf_token %} {% if cl.formset %} {{ cl.formset.management_form }} {% endif %} diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 42802f57bc..65e73c922d 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -22,7 +22,7 @@ {% else %}

{% blocktrans with object as escaped_object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}

    {{ deleted_objects|unordered_list }}
- + {% csrf_token %}
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html index 5550b73e2e..7f4fbc5726 100644 --- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html @@ -23,7 +23,7 @@ {% for deleteable_object in deletable_objects %}
    {{ deleteable_object|unordered_list }}
{% endfor %} - + {% csrf_token %}
{% for obj in queryset %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index d162e5a9fa..876c4b0327 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -14,7 +14,7 @@

{{ error_message }}

{% endif %}
- +{% csrf_token %}
diff --git a/django/contrib/admin/templates/admin/template_validator.html b/django/contrib/admin/templates/admin/template_validator.html index d221807486..9a139c5d49 100644 --- a/django/contrib/admin/templates/admin/template_validator.html +++ b/django/contrib/admin/templates/admin/template_validator.html @@ -4,7 +4,7 @@
- +{% csrf_token %} {% if form.errors %}

Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:

diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index c13c7f7040..6d7a6609de 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -11,7 +11,7 @@

{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}

- +{% csrf_token %} {{ form.old_password.errors }}

{{ form.old_password }}

diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html index 049ee625a9..df9cf1b316 100644 --- a/django/contrib/admin/templates/registration/password_reset_confirm.html +++ b/django/contrib/admin/templates/registration/password_reset_confirm.html @@ -13,7 +13,7 @@

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

- +{% csrf_token %} {{ form.new_password1.errors }}

{{ form.new_password1 }}

{{ form.new_password2.errors }} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index 704066c68a..d3a128428a 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -11,7 +11,7 @@

{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

- +{% csrf_token %} {{ form.email.errors }}

{{ form.email }}

diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 9a4ce3b266..5a02ab01be 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -106,6 +106,11 @@ def result_headers(cl): else: header = field_name header = header.replace('_', ' ') + # if the field is the action checkbox: no sorting and special class + if field_name == 'action_checkbox': + yield {"text": header, + "class_attrib": mark_safe(' class="action-checkbox-column"')} + continue # It is a non-field, but perhaps one that is sortable admin_order_field = getattr(attr, "admin_order_field", None) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 50e41437b8..726da650a6 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -149,12 +149,16 @@ def validate(cls, model): validate_inline(inline, cls, model) def validate_inline(cls, parent, parent_model): + # model is already verified to exist and be a Model if cls.fk_name: # default value is None f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name) if not isinstance(f, models.ForeignKey): raise ImproperlyConfigured("'%s.fk_name is not an instance of " "models.ForeignKey." % cls.__name__) + + fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + # extra = 3 # max_num = 0 for attr in ('extra', 'max_num'): @@ -169,7 +173,6 @@ def validate_inline(cls, parent, parent_model): # exclude if hasattr(cls, 'exclude') and cls.exclude: - fk = _get_foreign_key(parent_model, cls.model, can_fail=True) if fk and fk.name in cls.exclude: raise ImproperlyConfigured("%s cannot exclude the field " "'%s' - this is the foreign key to the parent model " @@ -193,6 +196,11 @@ def validate_base(cls, model): check_isseq(cls, 'fields', cls.fields) for field in cls.fields: check_formfield(cls, model, opts, 'fields', field) + f = get_field(cls, model, opts, 'fields', field) + if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: + raise ImproperlyConfigured("'%s.fields' can't include the ManyToManyField " + "field '%s' because '%s' manually specifies " + "a 'through' model." % (cls.__name__, field, field)) if cls.fieldsets: raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) if len(cls.fields) > len(set(cls.fields)): @@ -211,11 +219,28 @@ def validate_base(cls, model): raise ImproperlyConfigured("'fields' key is required in " "%s.fieldsets[%d][1] field options dict." % (cls.__name__, idx)) + for fields in fieldset[1]['fields']: + # The entry in fields might be a tuple. If it is a standalone + # field, make it into a tuple to make processing easier. + if type(fields) != tuple: + fields = (fields,) + for field in fields: + check_formfield(cls, model, opts, "fieldsets[%d][1]['fields']" % idx, field) + try: + f = opts.get_field(field) + if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: + raise ImproperlyConfigured("'%s.fieldsets[%d][1]['fields']' " + "can't include the ManyToManyField field '%s' because " + "'%s' manually specifies a 'through' model." % ( + cls.__name__, idx, field, field)) + except models.FieldDoesNotExist: + # If we can't find a field on the model that matches, + # it could be an extra field on the form. + pass flattened_fieldsets = flatten_fieldsets(cls.fieldsets) if len(flattened_fieldsets) > len(set(flattened_fieldsets)): raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - for field in flattened_fieldsets: - check_formfield(cls, model, opts, "fieldsets[%d][1]['fields']" % idx, field) + # form if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py index 842d589a54..6115edcfd0 100644 --- a/django/contrib/auth/tests/remote_user.py +++ b/django/contrib/auth/tests/remote_user.py @@ -2,7 +2,7 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.backends import RemoteUserBackend -from django.contrib.auth.models import AnonymousUser, User +from django.contrib.auth.models import User from django.test import TestCase @@ -30,15 +30,15 @@ class RemoteUserTest(TestCase): num_users = User.objects.count() response = self.client.get('/remote_user/') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) response = self.client.get('/remote_user/', REMOTE_USER=None) - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) response = self.client.get('/remote_user/', REMOTE_USER='') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) def test_unknown_user(self): @@ -115,7 +115,7 @@ class RemoteUserNoCreateTest(RemoteUserTest): def test_unknown_user(self): num_users = User.objects.count() response = self.client.get('/remote_user/', REMOTE_USER='newuser') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index f753ed6de8..d427874df0 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -4,6 +4,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm from django.contrib.auth.tokens import default_token_generator +from django.views.decorators.csrf import csrf_protect from django.core.urlresolvers import reverse from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.sites.models import Site, RequestSite @@ -14,11 +15,15 @@ from django.utils.translation import ugettext as _ from django.contrib.auth.models import User from django.views.decorators.cache import never_cache -def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME): +@csrf_protect +@never_cache +def login(request, template_name='registration/login.html', + redirect_field_name=REDIRECT_FIELD_NAME, + authentication_form=AuthenticationForm): "Displays the login form and handles the login action." redirect_to = request.REQUEST.get(redirect_field_name, '') if request.method == "POST": - form = AuthenticationForm(data=request.POST) + form = authentication_form(data=request.POST) if form.is_valid(): # Light security check -- make sure redirect_to isn't garbage. if not redirect_to or '//' in redirect_to or ' ' in redirect_to: @@ -29,7 +34,7 @@ def login(request, template_name='registration/login.html', redirect_field_name= request.session.delete_test_cookie() return HttpResponseRedirect(redirect_to) else: - form = AuthenticationForm(request) + form = authentication_form(request) request.session.set_test_cookie() if Site._meta.installed: current_site = Site.objects.get_current() @@ -41,7 +46,6 @@ def login(request, template_name='registration/login.html', redirect_field_name= 'site': current_site, 'site_name': current_site.name, }, context_instance=RequestContext(request)) -login = never_cache(login) def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME): "Logs out the user and displays 'You are logged out' message." @@ -78,6 +82,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N # prompts for a new password # - password_reset_complete shows a success message for the above +@csrf_protect def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', password_reset_form=PasswordResetForm, token_generator=default_token_generator, @@ -107,6 +112,7 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas def password_reset_done(request, template_name='registration/password_reset_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) +# Doesn't need csrf_protect since no-one can guess the URL def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None): @@ -137,28 +143,29 @@ def password_reset_confirm(request, uidb36=None, token=None, template_name='regi else: context_instance['validlink'] = False form = None - context_instance['form'] = form + context_instance['form'] = form return render_to_response(template_name, context_instance=context_instance) def password_reset_complete(request, template_name='registration/password_reset_complete.html'): return render_to_response(template_name, context_instance=RequestContext(request, {'login_url': settings.LOGIN_URL})) +@csrf_protect +@login_required def password_change(request, template_name='registration/password_change_form.html', - post_change_redirect=None): + post_change_redirect=None, password_change_form=PasswordChangeForm): if post_change_redirect is None: post_change_redirect = reverse('django.contrib.auth.views.password_change_done') if request.method == "POST": - form = PasswordChangeForm(request.user, request.POST) + form = password_change_form(user=request.user, data=request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(post_change_redirect) else: - form = PasswordChangeForm(request.user) + form = password_change_form(user=request.user) return render_to_response(template_name, { 'form': form, }, context_instance=RequestContext(request)) -password_change = login_required(password_change) def password_change_done(request, template_name='registration/password_change_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index c2f8e564f4..ede833f530 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin from django.contrib.comments.models import Comment -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from django.contrib.comments import get_model +from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete class CommentsAdmin(admin.ModelAdmin): fieldsets = ( @@ -22,6 +23,44 @@ class CommentsAdmin(admin.ModelAdmin): ordering = ('-submit_date',) raw_id_fields = ('user',) search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') + actions = ["flag_comments", "approve_comments", "remove_comments"] + + def get_actions(self, request): + actions = super(CommentsAdmin, self).get_actions(request) + # Only superusers should be able to delete the comments from the DB. + if not request.user.is_superuser: + actions.pop('delete_selected') + if not request.user.has_perm('comments.can_moderate'): + actions.pop('approve_comments') + actions.pop('remove_comments') + return actions + + def flag_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_flag, _("flagged")) + flag_comments.short_description = _("Flag selected comments") + + def approve_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_approve, _('approved')) + approve_comments.short_description = _("Approve selected comments") + + def remove_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_delete, _('removed')) + remove_comments.short_description = _("Remove selected comments") + + def _bulk_flag(self, request, queryset, action, description): + """ + Flag, approve, or remove some comments from an admin action. Actually + calls the `action` argument to perform the heavy lifting. + """ + n_comments = 0 + for comment in queryset: + action(request, comment) + n_comments += 1 + + msg = ungettext(u'1 comment was successfully %(action)s.', + u'%(count)s comments were successfully %(action)s.', + n_comments) + self.message_user(request, msg % {'count': n_comments, 'action': description}) # Only register the default admin if the model is the built-in comment model # (this won't be true if there's a custom comment app). diff --git a/django/contrib/comments/templates/comments/approve.html b/django/contrib/comments/templates/comments/approve.html index a4306a6fc2..1a3a3fd80c 100644 --- a/django/contrib/comments/templates/comments/approve.html +++ b/django/contrib/comments/templates/comments/approve.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really make this comment public?" %}

{{ comment|linebreaks }}
-
+ {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/delete.html b/django/contrib/comments/templates/comments/delete.html index 7d73eac979..5ff2add9c5 100644 --- a/django/contrib/comments/templates/comments/delete.html +++ b/django/contrib/comments/templates/comments/delete.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really remove this comment?" %}

{{ comment|linebreaks }}
- + {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/flag.html b/django/contrib/comments/templates/comments/flag.html index 08dbe0b0b0..0b9ab1ccb2 100644 --- a/django/contrib/comments/templates/comments/flag.html +++ b/django/contrib/comments/templates/comments/flag.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really flag this comment?" %}

{{ comment|linebreaks }}
- + {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/form.html b/django/contrib/comments/templates/comments/form.html index d8e248372f..30f031128c 100644 --- a/django/contrib/comments/templates/comments/form.html +++ b/django/contrib/comments/templates/comments/form.html @@ -1,5 +1,5 @@ {% load comments i18n %} - +{% csrf_token %} {% if next %}{% endif %} {% for field in form %} {% if field.is_hidden %} diff --git a/django/contrib/comments/templates/comments/moderation_queue.html b/django/contrib/comments/templates/comments/moderation_queue.html deleted file mode 100644 index 73012b3539..0000000000 --- a/django/contrib/comments/templates/comments/moderation_queue.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "admin/change_list.html" %} -{% load adminmedia i18n %} - -{% block title %}{% trans "Comment moderation queue" %}{% endblock %} - -{% block extrahead %} - {{ block.super }} - -{% endblock %} - -{% block branding %} -

{% trans "Comment moderation queue" %}

-{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} -{% if empty %} -

{% trans "No comments to moderate" %}.

-{% else %} -
-
- - - - - - - - - - - - - - - {% for comment in comments %} - - - - - - - - - - - {% endfor %} - -
{% trans "Action" %}{% trans "Name" %}{% trans "Comment" %}{% trans "Email" %}{% trans "URL" %}{% trans "Authenticated?" %}{% trans "IP Address" %}{% trans "Date posted" %}
- - - - -
- - -
-
{{ comment.name }}{{ comment.comment|truncatewords:"50" }}{{ comment.email }}{{ comment.url }} - {% if comment.user %}{% trans - {{ comment.ip_address }}{{ comment.submit_date|date:"F j, P" }}
-
-
-{% endif %} -{% endblock %} diff --git a/django/contrib/comments/templates/comments/preview.html b/django/contrib/comments/templates/comments/preview.html index d3884575f5..1b072a76f0 100644 --- a/django/contrib/comments/templates/comments/preview.html +++ b/django/contrib/comments/templates/comments/preview.html @@ -5,7 +5,7 @@ {% block content %} {% load comments %} -
+ {% csrf_token %} {% if next %}{% endif %} {% if form.errors %}

{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}

diff --git a/django/contrib/comments/urls.py b/django/contrib/comments/urls.py index 5caef9c7d4..2bfefa3e2d 100644 --- a/django/contrib/comments/urls.py +++ b/django/contrib/comments/urls.py @@ -7,7 +7,6 @@ urlpatterns = patterns('django.contrib.comments.views', url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'), url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'), url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'), - url(r'^moderate/$', 'moderation.moderation_queue', name='comments-moderation-queue'), url(r'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'), url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'), ) diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 89a3dd9bba..7fbe80eead 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -10,6 +10,7 @@ from django.utils.html import escape from django.views.decorators.http import require_POST from django.contrib import comments from django.contrib.comments import signals +from django.views.decorators.csrf import csrf_protect class CommentPostBadRequest(http.HttpResponseBadRequest): """ @@ -22,6 +23,8 @@ class CommentPostBadRequest(http.HttpResponseBadRequest): if settings.DEBUG: self.content = render_to_string("comments/400-debug.html", {"why": why}) +@csrf_protect +@require_POST def post_comment(request, next=None): """ Post a comment. @@ -116,8 +119,6 @@ def post_comment(request, next=None): return next_redirect(data, next, comment_done, c=comment._get_pk_val()) -post_comment = require_POST(post_comment) - comment_done = confirmation_view( template = "comments/posted.html", doc = """Display a "comment was posted" success page.""" diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py index 3334b0927e..73304ba416 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -3,12 +3,12 @@ from django.conf import settings from django.shortcuts import get_object_or_404, render_to_response from django.contrib.auth.decorators import login_required, permission_required from utils import next_redirect, confirmation_view -from django.core.paginator import Paginator, InvalidPage -from django.http import Http404 from django.contrib import comments from django.contrib.comments import signals +from django.views.decorators.csrf import csrf_protect -#@login_required +@csrf_protect +@login_required def flag(request, comment_id, next=None): """ Flags a comment. Confirmation on GET, action on POST. @@ -22,18 +22,7 @@ def flag(request, comment_id, next=None): # Flag on POST if request.method == 'POST': - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.SUGGEST_REMOVAL - ) - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_flag(request, comment) return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk) # Render a form on GET @@ -42,9 +31,9 @@ def flag(request, comment_id, next=None): {'comment': comment, "next": next}, template.RequestContext(request) ) -flag = login_required(flag) -#@permission_required("comments.delete_comment") +@csrf_protect +@permission_required("comments.can_moderate") def delete(request, comment_id, next=None): """ Deletes a comment. Confirmation on GET, action on POST. Requires the "can @@ -60,20 +49,7 @@ def delete(request, comment_id, next=None): # Delete on POST if request.method == 'POST': # Flag the comment as deleted instead of actually deleting it. - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.MODERATOR_DELETION - ) - comment.is_removed = True - comment.save() - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_delete(request, comment) return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk) # Render a form on GET @@ -82,9 +58,9 @@ def delete(request, comment_id, next=None): {'comment': comment, "next": next}, template.RequestContext(request) ) -delete = permission_required("comments.can_moderate")(delete) -#@permission_required("comments.can_moderate") +@csrf_protect +@permission_required("comments.can_moderate") def approve(request, comment_id, next=None): """ Approve a comment (that is, mark it as public and non-removed). Confirmation @@ -100,23 +76,7 @@ def approve(request, comment_id, next=None): # Delete on POST if request.method == 'POST': # Flag the comment as approved. - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.MODERATOR_APPROVAL, - ) - - comment.is_removed = False - comment.is_public = True - comment.save() - - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_approve(request, comment) return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk) # Render a form on GET @@ -126,69 +86,64 @@ def approve(request, comment_id, next=None): template.RequestContext(request) ) -approve = permission_required("comments.can_moderate")(approve) +# The following functions actually perform the various flag/aprove/delete +# actions. They've been broken out into seperate functions to that they +# may be called from admin actions. - -#@permission_required("comments.can_moderate") -def moderation_queue(request): +def perform_flag(request, comment): """ - Displays a list of unapproved comments to be approved. - - Templates: `comments/moderation_queue.html` - Context: - comments - Comments to be approved (paginated). - empty - Is the comment list empty? - is_paginated - Is there more than one page? - results_per_page - Number of comments per page - has_next - Is there a next page? - has_previous - Is there a previous page? - page - The current page number - next - The next page number - pages - Number of pages - hits - Total number of comments - page_range - Range of page numbers - + Actually perform the flagging of a comment from a request. """ - qs = comments.get_model().objects.filter(is_public=False, is_removed=False) - paginator = Paginator(qs, 100) + flag, created = comments.models.CommentFlag.objects.get_or_create( + comment = comment, + user = request.user, + flag = comments.models.CommentFlag.SUGGEST_REMOVAL + ) + signals.comment_was_flagged.send( + sender = comment.__class__, + comment = comment, + flag = flag, + created = created, + request = request, + ) - try: - page = int(request.GET.get("page", 1)) - except ValueError: - raise Http404 +def perform_delete(request, comment): + flag, created = comments.models.CommentFlag.objects.get_or_create( + comment = comment, + user = request.user, + flag = comments.models.CommentFlag.MODERATOR_DELETION + ) + comment.is_removed = True + comment.save() + signals.comment_was_flagged.send( + sender = comment.__class__, + comment = comment, + flag = flag, + created = created, + request = request, + ) - try: - comments_per_page = paginator.page(page) - except InvalidPage: - raise Http404 - return render_to_response("comments/moderation_queue.html", { - 'comments' : comments_per_page.object_list, - 'empty' : page == 1 and paginator.count == 0, - 'is_paginated': paginator.num_pages > 1, - 'results_per_page': 100, - 'has_next': comments_per_page.has_next(), - 'has_previous': comments_per_page.has_previous(), - 'page': page, - 'next': page + 1, - 'previous': page - 1, - 'pages': paginator.num_pages, - 'hits' : paginator.count, - 'page_range' : paginator.page_range - }, context_instance=template.RequestContext(request)) +def perform_approve(request, comment): + flag, created = comments.models.CommentFlag.objects.get_or_create( + comment = comment, + user = request.user, + flag = comments.models.CommentFlag.MODERATOR_APPROVAL, + ) -moderation_queue = permission_required("comments.can_moderate")(moderation_queue) + comment.is_removed = False + comment.is_public = True + comment.save() + + signals.comment_was_flagged.send( + sender = comment.__class__, + comment = comment, + flag = flag, + created = created, + request = request, + ) + +# Confirmation views. flag_done = confirmation_view( template = "comments/flagged.html", diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 8e172b07db..f6c463db04 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -105,8 +105,6 @@ class GenericRelation(RelatedField, Field): limit_choices_to=kwargs.pop('limit_choices_to', None), symmetrical=kwargs.pop('symmetrical', True)) - # By its very nature, a GenericRelation doesn't create a table. - self.creates_table = False # Override content-type/object-id field names on the related class self.object_id_field_name = kwargs.pop("object_id_field", "object_id") diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py index 0d0a8eca9e..4885cfcc3e 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -1,160 +1,7 @@ -""" -Cross Site Request Forgery Middleware. +from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware, CsrfResponseMiddleware +from django.views.decorators.csrf import csrf_exempt, csrf_view_exempt, csrf_response_exempt -This module provides a middleware that implements protection -against request forgeries from other sites. -""" - -import re -import itertools -try: - from functools import wraps -except ImportError: - from django.utils.functional import wraps # Python 2.3, 2.4 fallback. - -from django.conf import settings -from django.http import HttpResponseForbidden -from django.utils.hashcompat import md5_constructor -from django.utils.safestring import mark_safe - -_ERROR_MSG = mark_safe('

403 Forbidden

Cross Site Request Forgery detected. Request aborted.

') - -_POST_FORM_RE = \ - re.compile(r'(]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) - -_HTML_TYPES = ('text/html', 'application/xhtml+xml') - -def _make_token(session_id): - return md5_constructor(settings.SECRET_KEY + session_id).hexdigest() - -class CsrfViewMiddleware(object): - """ - Middleware that requires a present and correct csrfmiddlewaretoken - for POST requests that have an active session. - """ - def process_view(self, request, callback, callback_args, callback_kwargs): - if request.method == 'POST': - if getattr(callback, 'csrf_exempt', False): - return None - - if request.is_ajax(): - return None - - try: - session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] - except KeyError: - # No session, no check required - return None - - csrf_token = _make_token(session_id) - # check incoming token - try: - request_csrf_token = request.POST['csrfmiddlewaretoken'] - except KeyError: - return HttpResponseForbidden(_ERROR_MSG) - - if request_csrf_token != csrf_token: - return HttpResponseForbidden(_ERROR_MSG) - - return None - -class CsrfResponseMiddleware(object): - """ - Middleware that post-processes a response to add a - csrfmiddlewaretoken if the response/request have an active - session. - """ - def process_response(self, request, response): - if getattr(response, 'csrf_exempt', False): - return response - - csrf_token = None - try: - # This covers a corner case in which the outgoing response - # both contains a form and sets a session cookie. This - # really should not be needed, since it is best if views - # that create a new session (login pages) also do a - # redirect, as is done by all such view functions in - # Django. - cookie = response.cookies[settings.SESSION_COOKIE_NAME] - csrf_token = _make_token(cookie.value) - except KeyError: - # Normal case - look for existing session cookie - try: - session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] - csrf_token = _make_token(session_id) - except KeyError: - # no incoming or outgoing cookie - pass - - if csrf_token is not None and \ - response['Content-Type'].split(';')[0] in _HTML_TYPES: - - # ensure we don't add the 'id' attribute twice (HTML validity) - idattributes = itertools.chain(("id='csrfmiddlewaretoken'",), - itertools.repeat('')) - def add_csrf_field(match): - """Returns the matched tag plus the added element""" - return mark_safe(match.group() + "
" + \ - "
") - - # Modify any POST forms - response.content = _POST_FORM_RE.sub(add_csrf_field, response.content) - return response - -class CsrfMiddleware(CsrfViewMiddleware, CsrfResponseMiddleware): - """Django middleware that adds protection against Cross Site - Request Forgeries by adding hidden form fields to POST forms and - checking requests for the correct value. - - In the list of middlewares, SessionMiddleware is required, and - must come after this middleware. CsrfMiddleWare must come after - compression middleware. - - If a session ID cookie is present, it is hashed with the - SECRET_KEY setting to create an authentication token. This token - is added to all outgoing POST forms and is expected on all - incoming POST requests that have a session ID cookie. - - If you are setting cookies directly, instead of using Django's - session framework, this middleware will not work. - - CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware - and CsrfResponseMiddleware which can be used independently. - """ - pass - -def csrf_response_exempt(view_func): - """ - Modifies a view function so that its response is exempt - from the post-processing of the CSRF middleware. - """ - def wrapped_view(*args, **kwargs): - resp = view_func(*args, **kwargs) - resp.csrf_exempt = True - return resp - return wraps(view_func)(wrapped_view) - -def csrf_view_exempt(view_func): - """ - Marks a view function as being exempt from CSRF view protection. - """ - # We could just do view_func.csrf_exempt = True, but decorators - # are nicer if they don't have side-effects, so we return a new - # function. - def wrapped_view(*args, **kwargs): - return view_func(*args, **kwargs) - wrapped_view.csrf_exempt = True - return wraps(view_func)(wrapped_view) - -def csrf_exempt(view_func): - """ - Marks a view function as being exempt from the CSRF checks - and post processing. - - This is the same as using both the csrf_view_exempt and - csrf_response_exempt decorators. - """ - return csrf_response_exempt(csrf_view_exempt(view_func)) +import warnings +warnings.warn("This import for CSRF functionality is deprecated. Please use django.middleware.csrf for the middleware and django.views.decorators.csrf for decorators.", + PendingDeprecationWarning + ) diff --git a/django/contrib/csrf/tests.py b/django/contrib/csrf/tests.py deleted file mode 100644 index 3c533a01e6..0000000000 --- a/django/contrib/csrf/tests.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.test import TestCase -from django.http import HttpRequest, HttpResponse, HttpResponseForbidden -from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt -from django.conf import settings - - -def post_form_response(): - resp = HttpResponse(content=""" - -""", mimetype="text/html") - return resp - -def test_view(request): - return post_form_response() - -class CsrfMiddlewareTest(TestCase): - - _session_id = "1" - - def _get_GET_no_session_request(self): - return HttpRequest() - - def _get_GET_session_request(self): - req = self._get_GET_no_session_request() - req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id - return req - - def _get_POST_session_request(self): - req = self._get_GET_session_request() - req.method = "POST" - return req - - def _get_POST_no_session_request(self): - req = self._get_GET_no_session_request() - req.method = "POST" - return req - - def _get_POST_session_request_with_token(self): - req = self._get_POST_session_request() - req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id) - return req - - def _get_post_form_response(self): - return post_form_response() - - def _get_new_session_response(self): - resp = self._get_post_form_response() - resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id - return resp - - def _check_token_present(self, response): - self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id)) - - def get_view(self): - return test_view - - # Check the post processing - def test_process_response_no_session(self): - """ - Check the post-processor does nothing if no session active - """ - req = self._get_GET_no_session_request() - resp = self._get_post_form_response() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertEquals(resp_content, resp2.content) - - def test_process_response_existing_session(self): - """ - Check that the token is inserted if there is an existing session - """ - req = self._get_GET_session_request() - resp = self._get_post_form_response() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2) - - def test_process_response_new_session(self): - """ - Check that the token is inserted if there is a new session being started - """ - req = self._get_GET_no_session_request() # no session in request - resp = self._get_new_session_response() # but new session started - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2) - - def test_process_response_exempt_view(self): - """ - Check that no post processing is done for an exempt view - """ - req = self._get_POST_session_request() - resp = csrf_exempt(self.get_view())(req) - resp_content = resp.content - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertEquals(resp_content, resp2.content) - - # Check the request processing - def test_process_request_no_session(self): - """ - Check that if no session is present, the middleware does nothing. - to the incoming request. - """ - req = self._get_POST_no_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) - - def test_process_request_session_no_token(self): - """ - Check that if a session is present but no token, we get a 'forbidden' - """ - req = self._get_POST_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(HttpResponseForbidden, req2.__class__) - - def test_process_request_session_and_token(self): - """ - Check that if a session is present and a token, the middleware lets it through - """ - req = self._get_POST_session_request_with_token() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) - - def test_process_request_session_no_token_exempt_view(self): - """ - Check that if a session is present and no token, but the csrf_exempt - decorator has been applied to the view, the middleware lets it through - """ - req = self._get_POST_session_request() - req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {}) - self.assertEquals(None, req2) - - def test_ajax_exemption(self): - """ - Check that AJAX requests are automatically exempted. - """ - req = self._get_POST_session_request() - req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) diff --git a/django/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html index 194bbdd675..2f2de1f637 100644 --- a/django/contrib/formtools/templates/formtools/form.html +++ b/django/contrib/formtools/templates/formtools/form.html @@ -4,7 +4,7 @@ {% if form.errors %}

Please correct the following errors

{% else %}

Submit

{% endif %} -
+{% csrf_token %} {{ form }}
diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html index c53ce91724..eb88b1ec2e 100644 --- a/django/contrib/formtools/templates/formtools/preview.html +++ b/django/contrib/formtools/templates/formtools/preview.html @@ -15,7 +15,7 @@

Security hash: {{ hash_value }}

- +{% csrf_token %} {% for field in form %}{{ field.as_hidden }} {% endfor %} @@ -25,7 +25,7 @@

Or edit it again

- +{% csrf_token %} {{ form }}
diff --git a/django/contrib/formtools/tests.py b/django/contrib/formtools/tests.py index 86d40b963b..bc65a60fbe 100644 --- a/django/contrib/formtools/tests.py +++ b/django/contrib/formtools/tests.py @@ -147,15 +147,18 @@ class WizardPageTwoForm(forms.Form): class WizardClass(wizard.FormWizard): def render_template(self, *args, **kw): - return "" + return http.HttpResponse("") def done(self, request, cleaned_data): return http.HttpResponse(success_string) -class DummyRequest(object): +class DummyRequest(http.HttpRequest): def __init__(self, POST=None): + super(DummyRequest, self).__init__() self.method = POST and "POST" or "GET" - self.POST = POST + if POST is not None: + self.POST.update(POST) + self._dont_enforce_csrf_checks = True class WizardTests(TestCase): def test_step_starts_at_zero(self): diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index b075628c49..4729e155b8 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -14,6 +14,7 @@ from django.template.context import RequestContext from django.utils.hashcompat import md5_constructor from django.utils.translation import ugettext_lazy as _ from django.contrib.formtools.utils import security_hash +from django.views.decorators.csrf import csrf_protect class FormWizard(object): # Dictionary of extra template context variables. @@ -44,6 +45,7 @@ class FormWizard(object): # hook methods might alter self.form_list. return len(self.form_list) + @csrf_protect def __call__(self, request, *args, **kwargs): """ Main method that does all the hard work, conforming to the Django view diff --git a/django/contrib/gis/db/backend/postgis/__init__.py b/django/contrib/gis/db/backend/postgis/__init__.py index 7833376d1e..323fef3f95 100644 --- a/django/contrib/gis/db/backend/postgis/__init__.py +++ b/django/contrib/gis/db/backend/postgis/__init__.py @@ -18,18 +18,21 @@ SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True, distance_spheroid=DISTANCE_SPHEROID, envelope=ENVELOPE, extent=EXTENT, + extent3d=EXTENT3D, gis_terms=POSTGIS_TERMS, geojson=ASGEOJSON, gml=ASGML, intersection=INTERSECTION, kml=ASKML, length=LENGTH, + length3d=LENGTH3D, length_spheroid=LENGTH_SPHEROID, make_line=MAKE_LINE, mem_size=MEM_SIZE, num_geom=NUM_GEOM, num_points=NUM_POINTS, perimeter=PERIMETER, + perimeter3d=PERIMETER3D, point_on_surface=POINT_ON_SURFACE, scale=SCALE, select=GEOM_SELECT, diff --git a/django/contrib/gis/db/backend/postgis/adaptor.py b/django/contrib/gis/db/backend/postgis/adaptor.py index 7deada45b7..d8d4dfd4ea 100644 --- a/django/contrib/gis/db/backend/postgis/adaptor.py +++ b/django/contrib/gis/db/backend/postgis/adaptor.py @@ -2,7 +2,7 @@ This object provides quoting for GEOS geometries into PostgreSQL/PostGIS. """ -from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_WKB +from django.contrib.gis.db.backend.postgis.query import GEOM_FROM_EWKB from psycopg2 import Binary from psycopg2.extensions import ISQLQuote @@ -11,7 +11,7 @@ class PostGISAdaptor(object): "Initializes on the geometry." # Getting the WKB (in string form, to allow easy pickling of # the adaptor) and the SRID from the geometry. - self.wkb = str(geom.wkb) + self.ewkb = str(geom.ewkb) self.srid = geom.srid def __conform__(self, proto): @@ -30,7 +30,7 @@ class PostGISAdaptor(object): def getquoted(self): "Returns a properly quoted string for use in PostgreSQL/PostGIS." # Want to use WKB, so wrap with psycopg2 Binary() to quote properly. - return "%s(%s, %s)" % (GEOM_FROM_WKB, Binary(self.wkb), self.srid or -1) + return "%s(E%s)" % (GEOM_FROM_EWKB, Binary(self.ewkb)) def prepare_database_save(self, unused): return self diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 7491676057..3279f24b18 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -63,17 +63,21 @@ if MAJOR_VERSION >= 1: DISTANCE_SPHERE = get_func('distance_sphere') DISTANCE_SPHEROID = get_func('distance_spheroid') ENVELOPE = get_func('Envelope') - EXTENT = get_func('extent') + EXTENT = get_func('Extent') + EXTENT3D = get_func('Extent3D') GEOM_FROM_TEXT = get_func('GeomFromText') + GEOM_FROM_EWKB = get_func('GeomFromEWKB') GEOM_FROM_WKB = get_func('GeomFromWKB') INTERSECTION = get_func('Intersection') LENGTH = get_func('Length') + LENGTH3D = get_func('Length3D') LENGTH_SPHEROID = get_func('length_spheroid') MAKE_LINE = get_func('MakeLine') MEM_SIZE = get_func('mem_size') NUM_GEOM = get_func('NumGeometries') NUM_POINTS = get_func('npoints') PERIMETER = get_func('Perimeter') + PERIMETER3D = get_func('Perimeter3D') POINT_ON_SURFACE = get_func('PointOnSurface') SCALE = get_func('Scale') SNAP_TO_GRID = get_func('SnapToGrid') diff --git a/django/contrib/gis/db/models/aggregates.py b/django/contrib/gis/db/models/aggregates.py index 7c8ab694c4..fc359393b3 100644 --- a/django/contrib/gis/db/models/aggregates.py +++ b/django/contrib/gis/db/models/aggregates.py @@ -24,6 +24,9 @@ class Collect(GeoAggregate): class Extent(GeoAggregate): name = 'Extent' +class Extent3D(GeoAggregate): + name = 'Extent3D' + class MakeLine(GeoAggregate): name = 'MakeLine' diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index eac66f4a83..d3d7f6be97 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -34,6 +34,9 @@ class GeoManager(Manager): def extent(self, *args, **kwargs): return self.get_query_set().extent(*args, **kwargs) + def extent3d(self, *args, **kwargs): + return self.get_query_set().extent3d(*args, **kwargs) + def geojson(self, *args, **kwargs): return self.get_query_set().geojson(*args, **kwargs) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index ad2cd8ceda..d4bc206d9b 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -110,6 +110,14 @@ class GeoQuerySet(QuerySet): """ return self._spatial_aggregate(aggregates.Extent, **kwargs) + def extent3d(self, **kwargs): + """ + Returns the aggregate extent, in 3D, of the features in the + GeoQuerySet. It is returned as a 6-tuple, comprising: + (xmin, ymin, zmin, xmax, ymax, zmax). + """ + return self._spatial_aggregate(aggregates.Extent3D, **kwargs) + def geojson(self, precision=8, crs=False, bbox=False, **kwargs): """ Returns a GeoJSON representation of the geomtry field in a `geojson` @@ -524,12 +532,14 @@ class GeoQuerySet(QuerySet): else: dist_att = Distance.unit_attname(geo_field.units_name) - # Shortcut booleans for what distance function we're using. + # Shortcut booleans for what distance function we're using and + # whether the geometry field is 3D. distance = func == 'distance' length = func == 'length' perimeter = func == 'perimeter' if not (distance or length or perimeter): raise ValueError('Unknown distance function: %s' % func) + geom_3d = geo_field.dim == 3 # The field's get_db_prep_lookup() is used to get any # extra distance parameters. Here we set up the @@ -604,7 +614,7 @@ class GeoQuerySet(QuerySet): # some error checking is required. if not isinstance(geo_field, PointField): raise ValueError('Spherical distance calculation only supported on PointFields.') - if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': + if not str(SpatialBackend.Geometry(buffer(params[0].ewkb)).geom_type) == 'Point': raise ValueError('Spherical distance calculation only supported with Point Geometry parameters') # The `function` procedure argument needs to be set differently for # geodetic distance calculations. @@ -617,9 +627,16 @@ class GeoQuerySet(QuerySet): elif length or perimeter: procedure_fmt = '%(geo_col)s' if geodetic and length: - # There's no `length_sphere` + # There's no `length_sphere`, and `length_spheroid` also + # works on 3D geometries. procedure_fmt += ',%(spheroid)s' procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]}) + elif geom_3d and SpatialBackend.postgis: + # Use 3D variants of perimeter and length routines on PostGIS. + if perimeter: + procedure_args.update({'function' : SpatialBackend.perimeter3d}) + elif length: + procedure_args.update({'function' : SpatialBackend.length3d}) # Setting up the settings for `_spatial_attribute`. s = {'select_field' : DistanceField(dist_att), diff --git a/django/contrib/gis/db/models/sql/aggregates.py b/django/contrib/gis/db/models/sql/aggregates.py index b534288891..7e91869ca3 100644 --- a/django/contrib/gis/db/models/sql/aggregates.py +++ b/django/contrib/gis/db/models/sql/aggregates.py @@ -11,6 +11,9 @@ geo_template = '%(function)s(%(field)s)' def convert_extent(box): raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') +def convert_extent3d(box): + raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.') + def convert_geom(wkt, geo_field): raise NotImplementedError('Aggregate method not implemented for this spatial backend.') @@ -23,6 +26,14 @@ if SpatialBackend.postgis: xmax, ymax = map(float, ur.split()) return (xmin, ymin, xmax, ymax) + def convert_extent3d(box3d): + # Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)"; + # parsing out and returning as a 4-tuple. + ll, ur = box3d[6:-1].split(',') + xmin, ymin, zmin = map(float, ll.split()) + xmax, ymax, zmax = map(float, ur.split()) + return (xmin, ymin, zmin, xmax, ymax, zmax) + def convert_geom(hex, geo_field): if hex: return SpatialBackend.Geometry(hex) else: return None @@ -94,7 +105,7 @@ class Collect(GeoAggregate): sql_function = SpatialBackend.collect class Extent(GeoAggregate): - is_extent = True + is_extent = '2D' sql_function = SpatialBackend.extent if SpatialBackend.oracle: @@ -102,6 +113,10 @@ if SpatialBackend.oracle: Extent.conversion_class = GeomField Extent.sql_template = '%(function)s(%(field)s)' +class Extent3D(GeoAggregate): + is_extent = '3D' + sql_function = SpatialBackend.extent3d + class MakeLine(GeoAggregate): conversion_class = GeomField sql_function = SpatialBackend.make_line diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index 094fc5815f..1691637c1e 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -262,7 +262,10 @@ class GeoQuery(sql.Query): """ if isinstance(aggregate, self.aggregates_module.GeoAggregate): if aggregate.is_extent: - return self.aggregates_module.convert_extent(value) + if aggregate.is_extent == '3D': + return self.aggregates_module.convert_extent3d(value) + else: + return self.aggregates_module.convert_extent(value) else: return self.aggregates_module.convert_geom(value, aggregate.source) else: diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 05b824b95d..b301cc1c27 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -29,7 +29,7 @@ +proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs >>> print mpnt MULTIPOINT (-89.999930378602485 29.999797886557641,-89.999930378602485 29.999797886557641) - + The OGRGeomType class is to make it easy to specify an OGR geometry type: >>> from django.contrib.gis.gdal import OGRGeomType >>> gt1 = OGRGeomType(3) # Using an integer for the type @@ -78,7 +78,7 @@ class OGRGeometry(GDALBase): geom_input = buffer(a2b_hex(geom_input.upper())) str_instance = False - # Constructing the geometry, + # Constructing the geometry, if str_instance: # Checking if unicode if isinstance(geom_input, unicode): @@ -130,12 +130,12 @@ class OGRGeometry(GDALBase): self.__class__ = GEO_CLASSES[self.geom_type.num] @classmethod - def from_bbox(cls, bbox): + def from_bbox(cls, bbox): "Constructs a Polygon from a bounding box (4-tuple)." x0, y0, x1, y1 = bbox return OGRGeometry( 'POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))' % ( x0, y0, x0, y1, x1, y1, x1, y0, x0, y0) ) - + def __del__(self): "Deletes this Geometry." if self._ptr: capi.destroy_geom(self._ptr) @@ -179,10 +179,17 @@ class OGRGeometry(GDALBase): "Returns 0 for points, 1 for lines, and 2 for surfaces." return capi.get_dims(self.ptr) - @property - def coord_dim(self): + def _get_coord_dim(self): "Returns the coordinate dimension of the Geometry." - return capi.get_coord_dims(self.ptr) + return capi.get_coord_dim(self.ptr) + + def _set_coord_dim(self, dim): + "Sets the coordinate dimension of this Geometry." + if not dim in (2, 3): + raise ValueError('Geometry dimension must be either 2 or 3') + capi.set_coord_dim(self.ptr, dim) + + coord_dim = property(_get_coord_dim, _set_coord_dim) @property def geom_count(self): @@ -207,13 +214,7 @@ class OGRGeometry(GDALBase): @property def geom_type(self): "Returns the Type for this Geometry." - try: - return OGRGeomType(capi.get_geom_type(self.ptr)) - except OGRException: - # VRT datasources return an invalid geometry type - # number, but a valid name -- we'll try that instead. - # See: http://trac.osgeo.org/gdal/ticket/2491 - return OGRGeomType(capi.get_geom_name(self.ptr)) + return OGRGeomType(capi.get_geom_type(self.ptr)) @property def geom_name(self): @@ -237,7 +238,7 @@ class OGRGeometry(GDALBase): return self.envelope.tuple #### SpatialReference-related Properties #### - + # The SRS property def _get_srs(self): "Returns the Spatial Reference for this Geometry." @@ -249,11 +250,15 @@ class OGRGeometry(GDALBase): def _set_srs(self, srs): "Sets the SpatialReference for this geometry." + # Do not have to clone the `SpatialReference` object pointer because + # when it is assigned to this `OGRGeometry` it's internal OGR + # reference count is incremented, and will likewise be released + # (decremented) when this geometry's destructor is called. if isinstance(srs, SpatialReference): - srs_ptr = srs_api.clone_srs(srs.ptr) + srs_ptr = srs.ptr elif isinstance(srs, (int, long, basestring)): sr = SpatialReference(srs) - srs_ptr = srs_api.clone_srs(sr.ptr) + srs_ptr = sr.ptr else: raise TypeError('Cannot assign spatial reference with object of type: %s' % type(srs)) capi.assign_srs(self.ptr, srs_ptr) @@ -298,7 +303,7 @@ class OGRGeometry(GDALBase): Returns the GeoJSON representation of this Geometry (requires GDAL 1.5+). """ - if GEOJSON: + if GEOJSON: return capi.to_json(self.ptr) else: raise NotImplementedError('GeoJSON output only supported on GDAL 1.5+.') @@ -335,7 +340,7 @@ class OGRGeometry(GDALBase): def wkt(self): "Returns the WKT representation of the Geometry." return capi.to_wkt(self.ptr, byref(c_char_p())) - + #### Geometry Methods #### def clone(self): "Clones this OGR Geometry." @@ -363,6 +368,16 @@ class OGRGeometry(GDALBase): klone = self.clone() klone.transform(coord_trans) return klone + + # Have to get the coordinate dimension of the original geometry + # so it can be used to reset the transformed geometry's dimension + # afterwards. This is done because of GDAL bug (in versions prior + # to 1.7) that turns geometries 3D after transformation, see: + # http://trac.osgeo.org/gdal/changeset/17792 + orig_dim = self.coord_dim + + # Depending on the input type, use the appropriate OGR routine + # to perform the transformation. if isinstance(coord_trans, CoordTransform): capi.geom_transform(self.ptr, coord_trans.ptr) elif isinstance(coord_trans, SpatialReference): @@ -373,6 +388,10 @@ class OGRGeometry(GDALBase): else: raise TypeError('Transform only accepts CoordTransform, SpatialReference, string, and integer objects.') + # Setting with original dimension, see comment above. + if self.coord_dim != orig_dim: + self.coord_dim = orig_dim + def transform_to(self, srs): "For backwards-compatibility." self.transform(srs) @@ -391,7 +410,7 @@ class OGRGeometry(GDALBase): def intersects(self, other): "Returns True if this geometry intersects with the other." return self._topology(capi.ogr_intersects, other) - + def equals(self, other): "Returns True if this geometry is equivalent to the other." return self._topology(capi.ogr_equals, other) @@ -436,7 +455,7 @@ class OGRGeometry(GDALBase): @property def convex_hull(self): """ - Returns the smallest convex Polygon that contains all the points in + Returns the smallest convex Polygon that contains all the points in this Geometry. """ return self._geomgen(capi.geom_convex_hull) @@ -456,7 +475,7 @@ class OGRGeometry(GDALBase): return self._geomgen(capi.geom_intersection, other) def sym_difference(self, other): - """ + """ Returns a new geometry which is the symmetric difference of this geometry and the other. """ @@ -545,7 +564,7 @@ class LineString(OGRGeometry): def y(self): "Returns the Y coordinates in a list." return self._listarr(capi.gety) - + @property def z(self): "Returns the Z coordinates in a list." @@ -610,7 +629,7 @@ class GeometryCollection(OGRGeometry): raise OGRIndexError('index out of range: %s' % index) else: return OGRGeometry(capi.clone_geom(capi.get_geom_ref(self.ptr, index)), self.srs) - + def __iter__(self): "Iterates over each Geometry." for i in xrange(self.geom_count): @@ -658,5 +677,12 @@ GEO_CLASSES = {1 : Point, 5 : MultiLineString, 6 : MultiPolygon, 7 : GeometryCollection, - 101: LinearRing, + 101: LinearRing, + 1 + OGRGeomType.wkb25bit : Point, + 2 + OGRGeomType.wkb25bit : LineString, + 3 + OGRGeomType.wkb25bit : Polygon, + 4 + OGRGeomType.wkb25bit : MultiPoint, + 5 + OGRGeomType.wkb25bit : MultiLineString, + 6 + OGRGeomType.wkb25bit : MultiPolygon, + 7 + OGRGeomType.wkb25bit : GeometryCollection, } diff --git a/django/contrib/gis/gdal/geomtype.py b/django/contrib/gis/gdal/geomtype.py index b3309531c0..3bf94d4815 100644 --- a/django/contrib/gis/gdal/geomtype.py +++ b/django/contrib/gis/gdal/geomtype.py @@ -4,6 +4,8 @@ from django.contrib.gis.gdal.error import OGRException class OGRGeomType(object): "Encapulates OGR Geometry Types." + wkb25bit = -2147483648 + # Dictionary of acceptable OGRwkbGeometryType s and their string names. _types = {0 : 'Unknown', 1 : 'Point', @@ -15,6 +17,13 @@ class OGRGeomType(object): 7 : 'GeometryCollection', 100 : 'None', 101 : 'LinearRing', + 1 + wkb25bit: 'Point25D', + 2 + wkb25bit: 'LineString25D', + 3 + wkb25bit: 'Polygon25D', + 4 + wkb25bit: 'MultiPoint25D', + 5 + wkb25bit : 'MultiLineString25D', + 6 + wkb25bit : 'MultiPolygon25D', + 7 + wkb25bit : 'GeometryCollection25D', } # Reverse type dictionary, keyed by lower-case of the name. _str_types = dict([(v.lower(), k) for k, v in _types.items()]) @@ -68,7 +77,7 @@ class OGRGeomType(object): @property def django(self): "Returns the Django GeometryField for this OGR Type." - s = self.name + s = self.name.replace('25D', '') if s in ('LinearRing', 'None'): return None elif s == 'Unknown': diff --git a/django/contrib/gis/gdal/layer.py b/django/contrib/gis/gdal/layer.py index cf5e57866e..a2163bc3c8 100644 --- a/django/contrib/gis/gdal/layer.py +++ b/django/contrib/gis/gdal/layer.py @@ -1,5 +1,5 @@ # Needed ctypes routines -from ctypes import byref +from ctypes import c_double, byref # Other GDAL imports. from django.contrib.gis.gdal.base import GDALBase @@ -7,11 +7,12 @@ from django.contrib.gis.gdal.envelope import Envelope, OGREnvelope from django.contrib.gis.gdal.error import OGRException, OGRIndexError, SRSException from django.contrib.gis.gdal.feature import Feature from django.contrib.gis.gdal.field import OGRFieldTypes -from django.contrib.gis.gdal.geometries import OGRGeomType +from django.contrib.gis.gdal.geomtype import OGRGeomType +from django.contrib.gis.gdal.geometries import OGRGeometry from django.contrib.gis.gdal.srs import SpatialReference # GDAL ctypes function prototypes. -from django.contrib.gis.gdal.prototypes import ds as capi, srs as srs_api +from django.contrib.gis.gdal.prototypes import ds as capi, geom as geom_api, srs as srs_api # For more information, see the OGR C API source code: # http://www.gdal.org/ogr/ogr__api_8h.html @@ -156,6 +157,29 @@ class Layer(GDALBase): return [capi.get_field_precision(capi.get_field_defn(self._ldefn, i)) for i in xrange(self.num_fields)] + def _get_spatial_filter(self): + try: + return OGRGeometry(geom_api.clone_geom(capi.get_spatial_filter(self.ptr))) + except OGRException: + return None + + def _set_spatial_filter(self, filter): + if isinstance(filter, OGRGeometry): + capi.set_spatial_filter(self.ptr, filter.ptr) + elif isinstance(filter, (tuple, list)): + if not len(filter) == 4: + raise ValueError('Spatial filter list/tuple must have 4 elements.') + # Map c_double onto params -- if a bad type is passed in it + # will be caught here. + xmin, ymin, xmax, ymax = map(c_double, filter) + capi.set_spatial_filter_rect(self.ptr, xmin, ymin, xmax, ymax) + elif filter is None: + capi.set_spatial_filter(self.ptr, None) + else: + raise TypeError('Spatial filter must be either an OGRGeometry instance, a 4-tuple, or None.') + + spatial_filter = property(_get_spatial_filter, _set_spatial_filter) + #### Layer Methods #### def get_fields(self, field_name): """ diff --git a/django/contrib/gis/gdal/prototypes/ds.py b/django/contrib/gis/gdal/prototypes/ds.py index b64183eeb3..44828ee5f9 100644 --- a/django/contrib/gis/gdal/prototypes/ds.py +++ b/django/contrib/gis/gdal/prototypes/ds.py @@ -3,7 +3,7 @@ related data structures. OGR_Dr_*, OGR_DS_*, OGR_L_*, OGR_F_*, OGR_Fld_* routines are relevant here. """ -from ctypes import c_char_p, c_int, c_long, c_void_p, POINTER +from ctypes import c_char_p, c_double, c_int, c_long, c_void_p, POINTER from django.contrib.gis.gdal.envelope import OGREnvelope from django.contrib.gis.gdal.libgdal import lgdal from django.contrib.gis.gdal.prototypes.generation import \ @@ -38,6 +38,9 @@ get_layer_srs = srs_output(lgdal.OGR_L_GetSpatialRef, [c_void_p]) get_next_feature = voidptr_output(lgdal.OGR_L_GetNextFeature, [c_void_p]) reset_reading = void_output(lgdal.OGR_L_ResetReading, [c_void_p], errcheck=False) test_capability = int_output(lgdal.OGR_L_TestCapability, [c_void_p, c_char_p]) +get_spatial_filter = geom_output(lgdal.OGR_L_GetSpatialFilter, [c_void_p]) +set_spatial_filter = void_output(lgdal.OGR_L_SetSpatialFilter, [c_void_p, c_void_p], errcheck=False) +set_spatial_filter_rect = void_output(lgdal.OGR_L_SetSpatialFilterRect, [c_void_p, c_double, c_double, c_double, c_double], errcheck=False) ### Feature Definition Routines ### get_fd_geom_type = int_output(lgdal.OGR_FD_GetGeomType, [c_void_p]) diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py index 2898198d0d..e40f33e745 100644 --- a/django/contrib/gis/gdal/prototypes/geom.py +++ b/django/contrib/gis/gdal/prototypes/geom.py @@ -83,7 +83,8 @@ get_geom_srs = srs_output(lgdal.OGR_G_GetSpatialReference, [c_void_p]) get_area = double_output(lgdal.OGR_G_GetArea, [c_void_p]) get_centroid = void_output(lgdal.OGR_G_Centroid, [c_void_p, c_void_p]) get_dims = int_output(lgdal.OGR_G_GetDimension, [c_void_p]) -get_coord_dims = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) +get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) +set_coord_dim = void_output(lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False) get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p]) get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p]) diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 30ce462475..1abea785ca 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -1,13 +1,11 @@ import os, os.path, unittest -from django.contrib.gis.gdal import DataSource, Envelope, OGRException, OGRIndexError +from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRException, OGRIndexError from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString from django.contrib import gis # Path for SHP files data_path = os.path.join(os.path.dirname(gis.__file__), 'tests' + os.sep + 'data') def get_ds_file(name, ext): - - return os.sep.join([data_path, name, name + '.%s' % ext]) # Test SHP data source object @@ -25,7 +23,7 @@ ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver=' srs_wkt='GEOGCS["GCS_WGS_1984",DATUM["WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]', field_values={'dbl' : [float(i) for i in range(1, 6)], 'int' : range(1, 6), 'str' : [str(i) for i in range(1, 6)]}, fids=range(5)), - TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype=1, driver='VRT', + TestDS('test_vrt', ext='vrt', nfeat=3, nfld=3, geom='POINT', gtype='Point25D', driver='VRT', fields={'POINT_X' : OFTString, 'POINT_Y' : OFTString, 'NUM' : OFTString}, # VRT uses CSV, which all types are OFTString. extent=(1.0, 2.0, 100.0, 523.5), # Min/Max from CSV field_values={'POINT_X' : ['1.0', '5.0', '100.0'], 'POINT_Y' : ['2.0', '23.0', '523.5'], 'NUM' : ['5', '17', '23']}, @@ -191,7 +189,41 @@ class DataSourceTest(unittest.TestCase): if hasattr(source, 'srs_wkt'): self.assertEqual(source.srs_wkt, g.srs.wkt) + def test06_spatial_filter(self): + "Testing the Layer.spatial_filter property." + ds = DataSource(get_ds_file('cities', 'shp')) + lyr = ds[0] + # When not set, it should be None. + self.assertEqual(None, lyr.spatial_filter) + + # Must be set a/an OGRGeometry or 4-tuple. + self.assertRaises(TypeError, lyr._set_spatial_filter, 'foo') + + # Setting the spatial filter with a tuple/list with the extent of + # a buffer centering around Pueblo. + self.assertRaises(ValueError, lyr._set_spatial_filter, range(5)) + filter_extent = (-105.609252, 37.255001, -103.609252, 39.255001) + lyr.spatial_filter = (-105.609252, 37.255001, -103.609252, 39.255001) + self.assertEqual(OGRGeometry.from_bbox(filter_extent), lyr.spatial_filter) + feats = [feat for feat in lyr] + self.assertEqual(1, len(feats)) + self.assertEqual('Pueblo', feats[0].get('Name')) + + # Setting the spatial filter with an OGRGeometry for buffer centering + # around Houston. + filter_geom = OGRGeometry('POLYGON((-96.363151 28.763374,-94.363151 28.763374,-94.363151 30.763374,-96.363151 30.763374,-96.363151 28.763374))') + lyr.spatial_filter = filter_geom + self.assertEqual(filter_geom, lyr.spatial_filter) + feats = [feat for feat in lyr] + self.assertEqual(1, len(feats)) + self.assertEqual('Houston', feats[0].get('Name')) + + # Clearing the spatial filter by setting it to None. Now + # should indicate that there are 3 features in the Layer. + lyr.spatial_filter = None + self.assertEqual(3, len(lyr)) + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(DataSourceTest)) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index c920adc6c0..02305f97b4 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -46,6 +46,13 @@ class OGRGeomTest(unittest.TestCase): self.assertEqual(0, gt.num) self.assertEqual('Unknown', gt.name) + def test00b_geomtype_25d(self): + "Testing OGRGeomType object with 25D types." + wkb25bit = OGRGeomType.wkb25bit + self.failUnless(OGRGeomType(wkb25bit + 1) == 'Point25D') + self.failUnless(OGRGeomType('MultiLineString25D') == (5 + wkb25bit)) + self.assertEqual('GeometryCollectionField', OGRGeomType('GeometryCollection25D').django) + def test01a_wkt(self): "Testing WKT output." for g in wkt_out: @@ -319,6 +326,18 @@ class OGRGeomTest(unittest.TestCase): self.assertAlmostEqual(trans.x, p.x, prec) self.assertAlmostEqual(trans.y, p.y, prec) + def test09c_transform_dim(self): + "Testing coordinate dimension is the same on transformed geometries." + ls_orig = OGRGeometry('LINESTRING(-104.609 38.255)', 4326) + ls_trans = OGRGeometry('LINESTRING(992385.4472045 481455.4944650)', 2774) + + prec = 3 + ls_orig.transform(ls_trans.srs) + # Making sure the coordinate dimension is still 2D. + self.assertEqual(2, ls_orig.coord_dim) + self.assertAlmostEqual(ls_trans.x[0], ls_orig.x[0], prec) + self.assertAlmostEqual(ls_trans.y[0], ls_orig.y[0], prec) + def test10_difference(self): "Testing difference()." for i in xrange(len(topology_geoms)): @@ -406,6 +425,17 @@ class OGRGeomTest(unittest.TestCase): xmax, ymax = max(x), max(y) self.assertEqual((xmin, ymin, xmax, ymax), poly.extent) + def test16_25D(self): + "Testing 2.5D geometries." + pnt_25d = OGRGeometry('POINT(1 2 3)') + self.assertEqual('Point25D', pnt_25d.geom_type.name) + self.assertEqual(3.0, pnt_25d.z) + self.assertEqual(3, pnt_25d.coord_dim) + ls_25d = OGRGeometry('LINESTRING(1 1 1,2 2 2,3 3 3)') + self.assertEqual('LineString25D', ls_25d.geom_type.name) + self.assertEqual([1.0, 2.0, 3.0], ls_25d.z) + self.assertEqual(3, ls_25d.coord_dim) + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(OGRGeomTest)) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 866a852d49..68c116657c 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -357,33 +357,53 @@ class GEOSGeometry(GEOSBase, ListMixin): #### Output Routines #### @property def ewkt(self): - "Returns the EWKT (WKT + SRID) of the Geometry." + """ + Returns the EWKT (WKT + SRID) of the Geometry. Note that Z values + are *not* included in this representation because GEOS does not yet + support serializing them. + """ if self.get_srid(): return 'SRID=%s;%s' % (self.srid, self.wkt) else: return self.wkt @property def wkt(self): - "Returns the WKT (Well-Known Text) of the Geometry." + "Returns the WKT (Well-Known Text) representation of this Geometry." return wkt_w.write(self) @property def hex(self): """ - Returns the HEX of the Geometry -- please note that the SRID is not - included in this representation, because the GEOS C library uses - -1 by default, even if the SRID is set. + Returns the WKB of this Geometry in hexadecimal form. Please note + that the SRID and Z values are not included in this representation + because it is not a part of the OGC specification (use the `hexewkb` + property instead). """ # A possible faster, all-python, implementation: # str(self.wkb).encode('hex') return wkb_w.write_hex(self) + @property + def hexewkb(self): + """ + Returns the EWKB of this Geometry in hexadecimal form. This is an + extension of the WKB specification that includes SRID and Z values + that are a part of this geometry. + """ + if self.hasz: + if not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') + return ewkb_w3d.write_hex(self) + else: + return ewkb_w.write_hex(self) + @property def json(self): """ Returns GeoJSON representation of this Geometry if GDAL 1.5+ is installed. """ - if gdal.GEOJSON: + if gdal.GEOJSON: return self.ogr.json else: raise GEOSException('GeoJSON output only supported on GDAL 1.5+.') @@ -391,9 +411,28 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def wkb(self): - "Returns the WKB of the Geometry as a buffer." + """ + Returns the WKB (Well-Known Binary) representation of this Geometry + as a Python buffer. SRID and Z values are not included, use the + `ewkb` property instead. + """ return wkb_w.write(self) + @property + def ewkb(self): + """ + Return the EWKB representation of this Geometry as a Python buffer. + This is an extension of the WKB specification that includes any SRID + and Z values that are a part of this geometry. + """ + if self.hasz: + if not GEOS_PREPARE: + # See: http://trac.osgeo.org/geos/ticket/216 + raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') + return ewkb_w3d.write(self) + else: + return ewkb_w.write(self) + @property def kml(self): "Returns the KML representation of this Geometry." @@ -617,7 +656,7 @@ GEOS_CLASSES = {0 : Point, } # Similarly, import the GEOS I/O instances here to avoid conflicts. -from django.contrib.gis.geos.io import wkt_r, wkt_w, wkb_r, wkb_w +from django.contrib.gis.geos.io import wkt_r, wkt_w, wkb_r, wkb_w, ewkb_w, ewkb_w3d # If supported, import the PreparedGeometry class. if GEOS_PREPARE: diff --git a/django/contrib/gis/geos/io.py b/django/contrib/gis/geos/io.py index e5314c7911..2f895fbc2d 100644 --- a/django/contrib/gis/geos/io.py +++ b/django/contrib/gis/geos/io.py @@ -14,19 +14,19 @@ class IOBase(GEOSBase): "Base class for GEOS I/O objects." def __init__(self): # Getting the pointer with the constructor. - self.ptr = self.constructor() + self.ptr = self._constructor() def __del__(self): # Cleaning up with the appropriate destructor. - if self._ptr: self.destructor(self._ptr) + if self._ptr: self._destructor(self._ptr) ### WKT Reading and Writing objects ### # Non-public class for internal use because its `read` method returns # _pointers_ instead of a GEOSGeometry object. class _WKTReader(IOBase): - constructor = capi.wkt_reader_create - destructor = capi.wkt_reader_destroy + _constructor = capi.wkt_reader_create + _destructor = capi.wkt_reader_destroy ptr_type = capi.WKT_READ_PTR def read(self, wkt): @@ -39,8 +39,8 @@ class WKTReader(_WKTReader): return GEOSGeometry(super(WKTReader, self).read(wkt)) class WKTWriter(IOBase): - constructor = capi.wkt_writer_create - destructor = capi.wkt_writer_destroy + _constructor = capi.wkt_writer_create + _destructor = capi.wkt_writer_destroy ptr_type = capi.WKT_WRITE_PTR def write(self, geom): @@ -51,8 +51,8 @@ class WKTWriter(IOBase): # Non-public class for the same reason as _WKTReader above. class _WKBReader(IOBase): - constructor = capi.wkb_reader_create - destructor = capi.wkb_reader_destroy + _constructor = capi.wkb_reader_create + _destructor = capi.wkb_reader_destroy ptr_type = capi.WKB_READ_PTR def read(self, wkb): @@ -71,8 +71,8 @@ class WKBReader(_WKBReader): return GEOSGeometry(super(WKBReader, self).read(wkb)) class WKBWriter(IOBase): - constructor = capi.wkb_writer_create - destructor = capi.wkb_writer_destroy + _constructor = capi.wkb_writer_create + _destructor = capi.wkb_writer_destroy ptr_type = capi.WKB_WRITE_PTR def write(self, geom): @@ -121,3 +121,10 @@ wkt_r = _WKTReader() wkt_w = WKTWriter() wkb_r = _WKBReader() wkb_w = WKBWriter() + +# These instances are for writing EWKB in 2D and 3D. +ewkb_w = WKBWriter() +ewkb_w.srid = True +ewkb_w3d = WKBWriter() +ewkb_w3d.srid = True +ewkb_w3d.outdim = 3 diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py index a177f0df9f..e3f2417cd2 100644 --- a/django/contrib/gis/geos/prototypes/geom.py +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -62,17 +62,16 @@ def string_from_geom(func): ### ctypes prototypes ### -# Deprecated creation routines from WKB, HEX, WKT +# Deprecated creation and output routines from WKB, HEX, WKT from_hex = bin_constructor(lgeos.GEOSGeomFromHEX_buf) from_wkb = bin_constructor(lgeos.GEOSGeomFromWKB_buf) from_wkt = geom_output(lgeos.GEOSGeomFromWKT, [c_char_p]) -# Output routines to_hex = bin_output(lgeos.GEOSGeomToHEX_buf) to_wkb = bin_output(lgeos.GEOSGeomToWKB_buf) to_wkt = string_from_geom(lgeos.GEOSGeomToWKT) -# The GEOS geometry type, typeid, num_coordites and number of geometries +# The GEOS geometry type, typeid, num_coordinates and number of geometries geos_normalize = int_from_geom(lgeos.GEOSNormalize) geos_type = string_from_geom(lgeos.GEOSGeomType) geos_typeid = int_from_geom(lgeos.GEOSGeomTypeId) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 070ccf6d5f..440075dd49 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -71,6 +71,49 @@ class GEOSTest(unittest.TestCase): geom = fromstr(g.wkt) self.assertEqual(g.hex, geom.hex) + def test01b_hexewkb(self): + "Testing (HEX)EWKB output." + from binascii import a2b_hex + + pnt_2d = Point(0, 1, srid=4326) + pnt_3d = Point(0, 1, 2, srid=4326) + + # OGC-compliant HEX will not have SRID nor Z value. + self.assertEqual(ogc_hex, pnt_2d.hex) + self.assertEqual(ogc_hex, pnt_3d.hex) + + # HEXEWKB should be appropriate for its dimension -- have to use an + # a WKBWriter w/dimension set accordingly, else GEOS will insert + # garbage into 3D coordinate if there is none. Also, GEOS has a + # a bug in versions prior to 3.1 that puts the X coordinate in + # place of Z; an exception should be raised on those versions. + self.assertEqual(hexewkb_2d, pnt_2d.hexewkb) + if GEOS_PREPARE: + self.assertEqual(hexewkb_3d, pnt_3d.hexewkb) + self.assertEqual(True, GEOSGeometry(hexewkb_3d).hasz) + else: + try: + hexewkb = pnt_3d.hexewkb + except GEOSException: + pass + else: + self.fail('Should have raised GEOSException.') + + # Same for EWKB. + self.assertEqual(buffer(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) + if GEOS_PREPARE: + self.assertEqual(buffer(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) + else: + try: + ewkb = pnt_3d.ewkb + except GEOSException: + pass + else: + self.fail('Should have raised GEOSException') + + # Redundant sanity check. + self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) + def test01c_kml(self): "Testing KML output." for tg in wkt_out: diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 75b8fc9d0d..5b172a3cef 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -9,9 +9,10 @@ def geo_suite(): some backends). """ from django.conf import settings + from django.contrib.gis.geos import GEOS_PREPARE from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.utils import HAS_GEOIP - from django.contrib.gis.tests.utils import mysql + from django.contrib.gis.tests.utils import postgis, mysql # The test suite. s = unittest.TestSuite() @@ -32,6 +33,10 @@ def geo_suite(): if not mysql: test_apps.append('distapp') + # Only PostGIS using GEOS 3.1+ can support 3D so far. + if postgis and GEOS_PREPARE: + test_apps.append('geo3d') + if HAS_GDAL: # These tests require GDAL. test_suite_names.extend(['test_spatialrefsys', 'test_geoforms']) @@ -164,20 +169,3 @@ def run_tests(test_labels, verbosity=1, interactive=True, extra_tests=[], suite= # Returning the total failures and errors return len(result.failures) + len(result.errors) - -# Class for creating a fake module with a run method. This is for the -# GEOS and GDAL tests that were moved to their respective modules. -class _DeprecatedTestModule(object): - def __init__(self, mod_name): - self.mod_name = mod_name - - def run(self): - from warnings import warn - warn('This test module is deprecated because it has moved to ' \ - '`django.contrib.gis.%s.tests` and will disappear in 1.2.' % - self.mod_name, DeprecationWarning) - tests = import_module('django.contrib.gis.%s.tests' % self.mod_name) - tests.run() - -test_geos = _DeprecatedTestModule('geos') -test_gdal = _DeprecatedTestModule('gdal') diff --git a/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt b/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt index 85e6be8e27..979c179bb0 100644 --- a/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt +++ b/django/contrib/gis/tests/data/test_vrt/test_vrt.vrt @@ -1,7 +1,7 @@ test_vrt.csv -wkbPoint +wkbPoint25D \ No newline at end of file diff --git a/django/contrib/gis/tests/geo3d/__init__.py b/django/contrib/gis/tests/geo3d/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/tests/geo3d/models.py b/django/contrib/gis/tests/geo3d/models.py new file mode 100644 index 0000000000..3c4f77ee05 --- /dev/null +++ b/django/contrib/gis/tests/geo3d/models.py @@ -0,0 +1,69 @@ +from django.contrib.gis.db import models + +class City3D(models.Model): + name = models.CharField(max_length=30) + point = models.PointField(dim=3) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Interstate2D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(srid=4269) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Interstate3D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(dim=3, srid=4269) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class InterstateProj2D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class InterstateProj3D(models.Model): + name = models.CharField(max_length=30) + line = models.LineStringField(dim=3, srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Polygon2D(models.Model): + name = models.CharField(max_length=30) + poly = models.PolygonField(srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Polygon3D(models.Model): + name = models.CharField(max_length=30) + poly = models.PolygonField(dim=3, srid=32140) + objects = models.GeoManager() + + def __unicode__(self): + return self.name + +class Point2D(models.Model): + point = models.PointField() + objects = models.GeoManager() + +class Point3D(models.Model): + point = models.PointField(dim=3) + objects = models.GeoManager() + +class MultiPoint3D(models.Model): + mpoint = models.MultiPointField(dim=3) + objects = models.GeoManager() diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py new file mode 100644 index 0000000000..034a979a4c --- /dev/null +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -0,0 +1,234 @@ +import os, re, unittest +from django.contrib.gis.db.models import Union, Extent3D +from django.contrib.gis.geos import GEOSGeometry, Point, Polygon +from django.contrib.gis.utils import LayerMapping, LayerMapError + +from models import City3D, Interstate2D, Interstate3D, \ + InterstateProj2D, InterstateProj3D, \ + Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D + +data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data')) +city_file = os.path.join(data_path, 'cities', 'cities.shp') +vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt') + +# The coordinates of each city, with Z values corresponding to their +# altitude in meters. +city_data = ( + ('Houston', (-95.363151, 29.763374, 18)), + ('Dallas', (-96.801611, 32.782057, 147)), + ('Oklahoma City', (-97.521157, 34.464642, 380)), + ('Wellington', (174.783117, -41.315268, 14)), + ('Pueblo', (-104.609252, 38.255001, 1433)), + ('Lawrence', (-95.235060, 38.971823, 251)), + ('Chicago', (-87.650175, 41.850385, 181)), + ('Victoria', (-123.305196, 48.462611, 15)), +) + +# Reference mapping of city name to its altitude (Z value). +city_dict = dict((name, coords) for name, coords in city_data) + +# 3D freeway data derived from the National Elevation Dataset: +# http://seamless.usgs.gov/products/9arc.php +interstate_data = ( + ('I-45', + 'LINESTRING(-95.3708481 29.7765870 11.339,-95.3694580 29.7787980 4.536,-95.3690305 29.7797359 9.762,-95.3691886 29.7812450 12.448,-95.3696447 29.7850144 10.457,-95.3702511 29.7868518 9.418,-95.3706724 29.7881286 14.858,-95.3711632 29.7896157 15.386,-95.3714525 29.7936267 13.168,-95.3717848 29.7955007 15.104,-95.3717719 29.7969804 16.516,-95.3717305 29.7982117 13.923,-95.3717254 29.8000778 14.385,-95.3719875 29.8013539 15.160,-95.3720575 29.8026785 15.544,-95.3721321 29.8040912 14.975,-95.3722074 29.8050998 15.688,-95.3722779 29.8060430 16.099,-95.3733818 29.8076750 15.197,-95.3741563 29.8103686 17.268,-95.3749458 29.8129927 19.857,-95.3763564 29.8144557 15.435)', + ( 11.339, 4.536, 9.762, 12.448, 10.457, 9.418, 14.858, + 15.386, 13.168, 15.104, 16.516, 13.923, 14.385, 15.16 , + 15.544, 14.975, 15.688, 16.099, 15.197, 17.268, 19.857, + 15.435), + ), + ) + +# Bounding box polygon for inner-loop of Houston (in projected coordinate +# system 32140), with elevation values from the National Elevation Dataset +# (see above). +bbox_wkt = 'POLYGON((941527.97 4225693.20,962596.48 4226349.75,963152.57 4209023.95,942051.75 4208366.38,941527.97 4225693.20))' +bbox_z = (21.71, 13.21, 9.12, 16.40, 21.71) +def gen_bbox(): + bbox_2d = GEOSGeometry(bbox_wkt, srid=32140) + bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) + return bbox_2d, bbox_3d + +class Geo3DTest(unittest.TestCase): + """ + Only a subset of the PostGIS routines are 3D-enabled, and this TestCase + tries to test the features that can handle 3D and that are also + available within GeoDjango. For more information, see the PostGIS docs + on the routines that support 3D: + + http://postgis.refractions.net/documentation/manual-1.4/ch08.html#PostGIS_3D_Functions + """ + + def test01_3d(self): + "Test the creation of 3D models." + # 3D models for the rest of the tests will be populated in here. + # For each 3D data set create model (and 2D version if necessary), + # retrieve, and assert geometry is in 3D and contains the expected + # 3D values. + for name, pnt_data in city_data: + x, y, z = pnt_data + pnt = Point(x, y, z, srid=4326) + City3D.objects.create(name=name, point=pnt) + city = City3D.objects.get(name=name) + self.failUnless(city.point.hasz) + self.assertEqual(z, city.point.z) + + # Interstate (2D / 3D and Geographic/Projected variants) + for name, line, exp_z in interstate_data: + line_3d = GEOSGeometry(line, srid=4269) + # Using `hex` attribute because it omits 3D. + line_2d = GEOSGeometry(line_3d.hex, srid=4269) + + # Creating a geographic and projected version of the + # interstate in both 2D and 3D. + Interstate3D.objects.create(name=name, line=line_3d) + InterstateProj3D.objects.create(name=name, line=line_3d) + Interstate2D.objects.create(name=name, line=line_2d) + InterstateProj2D.objects.create(name=name, line=line_2d) + + # Retrieving and making sure it's 3D and has expected + # Z values -- shouldn't change because of coordinate system. + interstate = Interstate3D.objects.get(name=name) + interstate_proj = InterstateProj3D.objects.get(name=name) + for i in [interstate, interstate_proj]: + self.failUnless(i.line.hasz) + self.assertEqual(exp_z, tuple(i.line.z)) + + # Creating 3D Polygon. + bbox2d, bbox3d = gen_bbox() + Polygon2D.objects.create(name='2D BBox', poly=bbox2d) + Polygon3D.objects.create(name='3D BBox', poly=bbox3d) + p3d = Polygon3D.objects.get(name='3D BBox') + self.failUnless(p3d.poly.hasz) + self.assertEqual(bbox3d, p3d.poly) + + def test01a_3d_layermapping(self): + "Testing LayerMapping on 3D models." + from models import Point2D, Point3D + + point_mapping = {'point' : 'POINT'} + mpoint_mapping = {'mpoint' : 'MULTIPOINT'} + + # The VRT is 3D, but should still be able to map sans the Z. + lm = LayerMapping(Point2D, vrt_file, point_mapping, transform=False) + lm.save() + self.assertEqual(3, Point2D.objects.count()) + + # The city shapefile is 2D, and won't be able to fill the coordinates + # in the 3D model -- thus, a LayerMapError is raised. + self.assertRaises(LayerMapError, LayerMapping, + Point3D, city_file, point_mapping, transform=False) + + # 3D model should take 3D data just fine. + lm = LayerMapping(Point3D, vrt_file, point_mapping, transform=False) + lm.save() + self.assertEqual(3, Point3D.objects.count()) + + # Making sure LayerMapping.make_multi works right, by converting + # a Point25D into a MultiPoint25D. + lm = LayerMapping(MultiPoint3D, vrt_file, mpoint_mapping, transform=False) + lm.save() + self.assertEqual(3, MultiPoint3D.objects.count()) + + def test02a_kml(self): + "Test GeoQuerySet.kml() with Z values." + h = City3D.objects.kml(precision=6).get(name='Houston') + # KML should be 3D. + # `SELECT ST_AsKML(point, 6) FROM geo3d_city3d WHERE name = 'Houston';` + ref_kml_regex = re.compile(r'^-95.363\d+,29.763\d+,18$') + self.failUnless(ref_kml_regex.match(h.kml)) + + def test02b_geojson(self): + "Test GeoQuerySet.geojson() with Z values." + h = City3D.objects.geojson(precision=6).get(name='Houston') + # GeoJSON should be 3D + # `SELECT ST_AsGeoJSON(point, 6) FROM geo3d_city3d WHERE name='Houston';` + ref_json_regex = re.compile(r'^{"type":"Point","coordinates":\[-95.363151,29.763374,18(\.0+)?\]}$') + self.failUnless(ref_json_regex.match(h.geojson)) + + def test03a_union(self): + "Testing the Union aggregate of 3D models." + # PostGIS query that returned the reference EWKT for this test: + # `SELECT ST_AsText(ST_Union(point)) FROM geo3d_city3d;` + ref_ewkt = 'SRID=4326;MULTIPOINT(-123.305196 48.462611 15,-104.609252 38.255001 1433,-97.521157 34.464642 380,-96.801611 32.782057 147,-95.363151 29.763374 18,-95.23506 38.971823 251,-87.650175 41.850385 181,174.783117 -41.315268 14)' + ref_union = GEOSGeometry(ref_ewkt) + union = City3D.objects.aggregate(Union('point'))['point__union'] + self.failUnless(union.hasz) + self.assertEqual(ref_union, union) + + def test03b_extent(self): + "Testing the Extent3D aggregate for 3D models." + # `SELECT ST_Extent3D(point) FROM geo3d_city3d;` + ref_extent3d = (-123.305196, -41.315268, 14,174.783117, 48.462611, 1433) + extent1 = City3D.objects.aggregate(Extent3D('point'))['point__extent3d'] + extent2 = City3D.objects.extent3d() + + def check_extent3d(extent3d, tol=6): + for ref_val, ext_val in zip(ref_extent3d, extent3d): + self.assertAlmostEqual(ref_val, ext_val, tol) + + for e3d in [extent1, extent2]: + check_extent3d(e3d) + + def test04_perimeter(self): + "Testing GeoQuerySet.perimeter() on 3D fields." + # Reference query for values below: + # `SELECT ST_Perimeter3D(poly), ST_Perimeter2D(poly) FROM geo3d_polygon3d;` + ref_perim_3d = 76859.2620451 + ref_perim_2d = 76859.2577803 + tol = 6 + self.assertAlmostEqual(ref_perim_2d, + Polygon2D.objects.perimeter().get(name='2D BBox').perimeter.m, + tol) + self.assertAlmostEqual(ref_perim_3d, + Polygon3D.objects.perimeter().get(name='3D BBox').perimeter.m, + tol) + + def test05_length(self): + "Testing GeoQuerySet.length() on 3D fields." + # ST_Length_Spheroid Z-aware, and thus does not need to use + # a separate function internally. + # `SELECT ST_Length_Spheroid(line, 'SPHEROID["GRS 1980",6378137,298.257222101]') + # FROM geo3d_interstate[2d|3d];` + tol = 3 + ref_length_2d = 4368.1721949481 + ref_length_3d = 4368.62547052088 + self.assertAlmostEqual(ref_length_2d, + Interstate2D.objects.length().get(name='I-45').length.m, + tol) + self.assertAlmostEqual(ref_length_3d, + Interstate3D.objects.length().get(name='I-45').length.m, + tol) + + # Making sure `ST_Length3D` is used on for a projected + # and 3D model rather than `ST_Length`. + # `SELECT ST_Length(line) FROM geo3d_interstateproj2d;` + ref_length_2d = 4367.71564892392 + # `SELECT ST_Length3D(line) FROM geo3d_interstateproj3d;` + ref_length_3d = 4368.16897234101 + self.assertAlmostEqual(ref_length_2d, + InterstateProj2D.objects.length().get(name='I-45').length.m, + tol) + self.assertAlmostEqual(ref_length_3d, + InterstateProj3D.objects.length().get(name='I-45').length.m, + tol) + + def test06_scale(self): + "Testing GeoQuerySet.scale() on Z values." + # Mapping of City name to reference Z values. + zscales = (-3, 4, 23) + for zscale in zscales: + for city in City3D.objects.scale(1.0, 1.0, zscale): + self.assertEqual(city_dict[city.name][2] * zscale, city.scale.z) + + def test07_translate(self): + "Testing GeoQuerySet.translate() on Z values." + ztranslations = (5.23, 23, -17) + for ztrans in ztranslations: + for city in City3D.objects.translate(0, 0, ztrans): + self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z) + +def suite(): + s = unittest.TestSuite() + s.addTest(unittest.makeSuite(Geo3DTest)) + return s diff --git a/django/contrib/gis/tests/geo3d/views.py b/django/contrib/gis/tests/geo3d/views.py new file mode 100644 index 0000000000..60f00ef0ef --- /dev/null +++ b/django/contrib/gis/tests/geo3d/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index 043dace769..7efa4e6206 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -28,8 +28,11 @@ class GeoRegressionTests(unittest.TestCase): kmz = render_to_kmz('gis/kml/placemarks.kml', {'places' : places}) @no_spatialite + @no_mysql def test03_extent(self): "Testing `extent` on a table with a single point, see #11827." pnt = City.objects.get(name='Pueblo').point ref_ext = (pnt.x, pnt.y, pnt.x, pnt.y) - self.assertEqual(ref_ext, City.objects.filter(name='Pueblo').extent()) + extent = City.objects.filter(name='Pueblo').extent() + for ref_val, val in zip(ref_ext, extent): + self.assertAlmostEqual(ref_val, val, 4) diff --git a/django/contrib/gis/tests/geometries.py b/django/contrib/gis/tests/geometries.py index 950ffdb0e5..701741316d 100644 --- a/django/contrib/gis/tests/geometries.py +++ b/django/contrib/gis/tests/geometries.py @@ -171,3 +171,10 @@ json_geoms = (TestGeom('POINT(100 0)', json='{ "type": "Point", "coordinates": [ not_equal=True, ), ) + +# For testing HEX(EWKB). +ogc_hex = '01010000000000000000000000000000000000F03F' +# `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` +hexewkb_2d = '0101000020E61000000000000000000000000000000000F03F' +# `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` +hexewkb_3d = '01010000A0E61000000000000000000000000000000000F03F0000000000000040' diff --git a/django/contrib/gis/tests/layermap/models.py b/django/contrib/gis/tests/layermap/models.py index 5dbd528030..3a34d16f3f 100644 --- a/django/contrib/gis/tests/layermap/models.py +++ b/django/contrib/gis/tests/layermap/models.py @@ -29,6 +29,20 @@ class Interstate(models.Model): path = models.LineStringField() objects = models.GeoManager() +# Same as `City` above, but for testing model inheritance. +class CityBase(models.Model): + name = models.CharField(max_length=25) + population = models.IntegerField() + density = models.DecimalField(max_digits=7, decimal_places=1) + point = models.PointField() + objects = models.GeoManager() + +class ICity1(CityBase): + dt = models.DateField() + +class ICity2(ICity1): + dt_time = models.DateTimeField(auto_now=True) + # Mapping dictionaries for the models above. co_mapping = {'name' : 'Name', 'state' : {'name' : 'State'}, # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case). diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 56a9f2bdfe..b17e7b92fc 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,7 +1,7 @@ import os, unittest from copy import copy from decimal import Decimal -from models import City, County, CountyFeat, Interstate, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping +from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey from django.contrib.gis.gdal import DataSource @@ -242,6 +242,26 @@ class LayerMapTest(unittest.TestCase): lm.save(step=st, strict=True) self.county_helper(county_feat=False) + def test06_model_inheritance(self): + "Tests LayerMapping on inherited models. See #12093." + icity_mapping = {'name' : 'Name', + 'population' : 'Population', + 'density' : 'Density', + 'point' : 'POINT', + 'dt' : 'Created', + } + + # Parent model has geometry field. + lm1 = LayerMapping(ICity1, city_shp, icity_mapping) + lm1.save() + + # Grandparent has geometry field. + lm2 = LayerMapping(ICity2, city_shp, icity_mapping) + lm2.save() + + self.assertEqual(6, ICity1.objects.count()) + self.assertEqual(3, ICity2.objects.count()) + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(LayerMapTest)) diff --git a/django/contrib/gis/utils/__init__.py b/django/contrib/gis/utils/__init__.py index 2c9f2f3ce6..f336bcadbf 100644 --- a/django/contrib/gis/utils/__init__.py +++ b/django/contrib/gis/utils/__init__.py @@ -10,7 +10,7 @@ if HAS_GDAL: try: # LayerMapping requires DJANGO_SETTINGS_MODULE to be set, # so this needs to be in try/except. - from django.contrib.gis.utils.layermapping import LayerMapping + from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError except: pass diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 43bc70a0ec..e2c66740eb 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -133,6 +133,9 @@ class LayerMapping(object): MULTI_TYPES = {1 : OGRGeomType('MultiPoint'), 2 : OGRGeomType('MultiLineString'), 3 : OGRGeomType('MultiPolygon'), + OGRGeomType('Point25D').num : OGRGeomType('MultiPoint25D'), + OGRGeomType('LineString25D').num : OGRGeomType('MultiLineString25D'), + OGRGeomType('Polygon25D').num : OGRGeomType('MultiPolygon25D'), } # Acceptable Django field types and corresponding acceptable OGR @@ -282,19 +285,28 @@ class LayerMapping(object): if self.geom_field: raise LayerMapError('LayerMapping does not support more than one GeometryField per model.') + # Getting the coordinate dimension of the geometry field. + coord_dim = model_field.dim + try: - gtype = OGRGeomType(ogr_name) + if coord_dim == 3: + gtype = OGRGeomType(ogr_name + '25D') + else: + gtype = OGRGeomType(ogr_name) except OGRException: raise LayerMapError('Invalid mapping for GeometryField "%s".' % field_name) # Making sure that the OGR Layer's Geometry is compatible. ltype = self.layer.geom_type - if not (gtype == ltype or self.make_multi(ltype, model_field)): - raise LayerMapError('Invalid mapping geometry; model has %s, feature has %s.' % (fld_name, gtype)) + if not (ltype.name.startswith(gtype.name) or self.make_multi(ltype, model_field)): + raise LayerMapError('Invalid mapping geometry; model has %s%s, layer is %s.' % + (fld_name, (coord_dim == 3 and '(dim=3)') or '', ltype)) # Setting the `geom_field` attribute w/the name of the model field - # that is a Geometry. + # that is a Geometry. Also setting the coordinate dimension + # attribute. self.geom_field = field_name + self.coord_dim = coord_dim fields_val = model_field elif isinstance(model_field, models.ForeignKey): if isinstance(ogr_name, dict): @@ -482,6 +494,10 @@ class LayerMapping(object): if necessary (for example if the model field is MultiPolygonField while the mapped shapefile only contains Polygons). """ + # Downgrade a 3D geom to a 2D one, if necessary. + if self.coord_dim != geom.coord_dim: + geom.coord_dim = self.coord_dim + if self.make_multi(geom.geom_type, model_field): # Constructing a multi-geometry type to contain the single geometry multi_type = self.MULTI_TYPES[geom.geom_type.num] @@ -514,16 +530,26 @@ class LayerMapping(object): def geometry_column(self): "Returns the GeometryColumn model associated with the geographic column." from django.contrib.gis.models import GeometryColumns - # Getting the GeometryColumn object. + # Use the `get_field_by_name` on the model's options so that we + # get the correct model if there's model inheritance -- otherwise + # the returned model is None. + opts = self.model._meta + fld, model, direct, m2m = opts.get_field_by_name(self.geom_field) + if model is None: model = self.model + + # Trying to get the `GeometryColumns` object that corresponds to the + # the geometry field. try: - db_table = self.model._meta.db_table - geo_col = self.geom_field + db_table = model._meta.db_table + geo_col = fld.column + if SpatialBackend.oracle: # Making upper case for Oracle. db_table = db_table.upper() geo_col = geo_col.upper() - gc_kwargs = {GeometryColumns.table_name_col() : db_table, - GeometryColumns.geom_col_name() : geo_col, + + gc_kwargs = { GeometryColumns.table_name_col() : db_table, + GeometryColumns.geom_col_name() : geo_col, } return GeometryColumns.objects.get(**gc_kwargs) except Exception, msg: diff --git a/django/core/context_processors.py b/django/core/context_processors.py index cb07125ce7..b950dba0f6 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -8,6 +8,8 @@ RequestContext. """ from django.conf import settings +from django.middleware.csrf import get_token +from django.utils.functional import lazy, memoize, SimpleLazyObject def auth(request): """ @@ -17,17 +19,46 @@ def auth(request): If there is no 'user' attribute in the request, uses AnonymousUser (from django.contrib.auth). """ - if hasattr(request, 'user'): - user = request.user - else: - from django.contrib.auth.models import AnonymousUser - user = AnonymousUser() + # If we access request.user, request.session is accessed, which results in + # 'Vary: Cookie' being sent in every request that uses this context + # processor, which can easily be every request on a site if + # TEMPLATE_CONTEXT_PROCESSORS has this context processor added. This kills + # the ability to cache. So, we carefully ensure these attributes are lazy. + # We don't use django.utils.functional.lazy() for User, because that + # requires knowing the class of the object we want to proxy, which could + # break with custom auth backends. LazyObject is a less complete but more + # flexible solution that is a good enough wrapper for 'User'. + def get_user(): + if hasattr(request, 'user'): + return request.user + else: + from django.contrib.auth.models import AnonymousUser + return AnonymousUser() + return { - 'user': user, - 'messages': user.get_and_delete_messages(), - 'perms': PermWrapper(user), + 'user': SimpleLazyObject(get_user), + 'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(), + 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(), } +def csrf(request): + """ + Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if + it has not been provided by either a view decorator or the middleware + """ + def _get_val(): + token = get_token(request) + if token is None: + # In order to be able to provide debugging info in the + # case of misconfiguration, we use a sentinel value + # instead of returning an empty dict. + return 'NOTPROVIDED' + else: + return token + _get_val = lazy(_get_val, str) + + return {'csrf_token': _get_val() } + def debug(request): "Returns context variables helpful for debugging." context_extras = {} @@ -79,7 +110,7 @@ class PermWrapper(object): def __getitem__(self, module_name): return PermLookupDict(self.user, module_name) - + def __iter__(self): # I am large, I contain multitudes. raise TypeError("PermWrapper is not iterable.") diff --git a/django/core/files/storage.py b/django/core/files/storage.py index e183c17dd3..96c0b54623 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -118,10 +118,6 @@ class Storage(object): """ raise NotImplementedError() - # Needed by django.utils.functional.LazyObject (via DefaultStorage). - def get_all_members(self): - return self.__members__ - class FileSystemStorage(Storage): """ Standard filesystem storage diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index e6ef6e2f9e..f144ce4bb1 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -68,6 +68,9 @@ class BaseHandler(object): from django.core import exceptions, urlresolvers from django.conf import settings + # Reset the urlconf for this thread. + urlresolvers.set_urlconf(None) + # Apply request middleware for middleware_method in self._request_middleware: response = middleware_method(request) @@ -77,61 +80,69 @@ class BaseHandler(object): # Get urlconf from request object, if available. Otherwise use default. urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF) + # Set the urlconf for this thread to the one specified above. + urlresolvers.set_urlconf(urlconf) + resolver = urlresolvers.RegexURLResolver(r'^/', urlconf) try: - callback, callback_args, callback_kwargs = resolver.resolve( - request.path_info) - - # Apply view middleware - for middleware_method in self._view_middleware: - response = middleware_method(request, callback, callback_args, callback_kwargs) - if response: - return response - try: - response = callback(request, *callback_args, **callback_kwargs) - except Exception, e: - # If the view raised an exception, run it through exception - # middleware, and if the exception middleware returns a - # response, use that. Otherwise, reraise the exception. - for middleware_method in self._exception_middleware: - response = middleware_method(request, e) + callback, callback_args, callback_kwargs = resolver.resolve( + request.path_info) + + # Apply view middleware + for middleware_method in self._view_middleware: + response = middleware_method(request, callback, callback_args, callback_kwargs) if response: return response - raise - # Complain if the view returned None (a common error). - if response is None: try: - view_name = callback.func_name # If it's a function - except AttributeError: - view_name = callback.__class__.__name__ + '.__call__' # If it's a class - raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name) + response = callback(request, *callback_args, **callback_kwargs) + except Exception, e: + # If the view raised an exception, run it through exception + # middleware, and if the exception middleware returns a + # response, use that. Otherwise, reraise the exception. + for middleware_method in self._exception_middleware: + response = middleware_method(request, e) + if response: + return response + raise - return response - except http.Http404, e: - if settings.DEBUG: - from django.views import debug - return debug.technical_404_response(request, e) - else: - try: - callback, param_dict = resolver.resolve404() - return callback(request, **param_dict) - except: + # Complain if the view returned None (a common error). + if response is None: try: - return self.handle_uncaught_exception(request, resolver, sys.exc_info()) - finally: - receivers = signals.got_request_exception.send(sender=self.__class__, request=request) - except exceptions.PermissionDenied: - return http.HttpResponseForbidden('

Permission denied

') - except SystemExit: - # Allow sys.exit() to actually exit. See tickets #1023 and #4701 - raise - except: # Handle everything else, including SuspiciousOperation, etc. - # Get the exception info now, in case another exception is thrown later. - exc_info = sys.exc_info() - receivers = signals.got_request_exception.send(sender=self.__class__, request=request) - return self.handle_uncaught_exception(request, resolver, exc_info) + view_name = callback.func_name # If it's a function + except AttributeError: + view_name = callback.__class__.__name__ + '.__call__' # If it's a class + raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name) + + return response + except http.Http404, e: + if settings.DEBUG: + from django.views import debug + return debug.technical_404_response(request, e) + else: + try: + callback, param_dict = resolver.resolve404() + return callback(request, **param_dict) + except: + try: + return self.handle_uncaught_exception(request, resolver, sys.exc_info()) + finally: + receivers = signals.got_request_exception.send(sender=self.__class__, request=request) + except exceptions.PermissionDenied: + return http.HttpResponseForbidden('

Permission denied

') + except SystemExit: + # Allow sys.exit() to actually exit. See tickets #1023 and #4701 + raise + except: # Handle everything else, including SuspiciousOperation, etc. + # Get the exception info now, in case another exception is thrown later. + exc_info = sys.exc_info() + receivers = signals.got_request_exception.send(sender=self.__class__, request=request) + return self.handle_uncaught_exception(request, resolver, exc_info) + finally: + # Reset URLconf for this thread on the way out for complete + # isolation of request.urlconf + urlresolvers.set_urlconf(None) def handle_uncaught_exception(self, request, resolver, exc_info): """ diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py new file mode 100644 index 0000000000..b02575793d --- /dev/null +++ b/django/core/mail/__init__.py @@ -0,0 +1,110 @@ +""" +Tools for sending email. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.importlib import import_module + +# Imported for backwards compatibility, and for the sake +# of a cleaner namespace. These symbols used to be in +# django/core/mail.py before the introduction of email +# backends and the subsequent reorganization (See #10355) +from django.core.mail.utils import CachedDnsName, DNS_NAME +from django.core.mail.message import \ + EmailMessage, EmailMultiAlternatives, \ + SafeMIMEText, SafeMIMEMultipart, \ + DEFAULT_ATTACHMENT_MIME_TYPE, make_msgid, \ + BadHeaderError, forbid_multi_line_headers +from django.core.mail.backends.smtp import EmailBackend as _SMTPConnection + +def get_connection(backend=None, fail_silently=False, **kwds): + """Load an e-mail backend and return an instance of it. + + If backend is None (default) settings.EMAIL_BACKEND is used. + + Both fail_silently and other keyword arguments are used in the + constructor of the backend. + """ + path = backend or settings.EMAIL_BACKEND + try: + mod = import_module(path) + except ImportError, e: + raise ImproperlyConfigured(('Error importing email backend %s: "%s"' + % (path, e))) + try: + cls = getattr(mod, 'EmailBackend') + except AttributeError: + raise ImproperlyConfigured(('Module "%s" does not define a ' + '"EmailBackend" class' % path)) + return cls(fail_silently=fail_silently, **kwds) + + +def send_mail(subject, message, from_email, recipient_list, + fail_silently=False, auth_user=None, auth_password=None, + connection=None): + """ + Easy wrapper for sending a single message to a recipient list. All members + of the recipient list will see the other recipients in the 'To' field. + + If auth_user is None, the EMAIL_HOST_USER setting is used. + If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. + + Note: The API for this method is frozen. New code wanting to extend the + functionality should use the EmailMessage class directly. + """ + connection = connection or get_connection(username=auth_user, + password=auth_password, + fail_silently=fail_silently) + return EmailMessage(subject, message, from_email, recipient_list, + connection=connection).send() + + +def send_mass_mail(datatuple, fail_silently=False, auth_user=None, + auth_password=None, connection=None): + """ + Given a datatuple of (subject, message, from_email, recipient_list), sends + each message to each recipient list. Returns the number of e-mails sent. + + If from_email is None, the DEFAULT_FROM_EMAIL setting is used. + If auth_user and auth_password are set, they're used to log in. + If auth_user is None, the EMAIL_HOST_USER setting is used. + If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. + + Note: The API for this method is frozen. New code wanting to extend the + functionality should use the EmailMessage class directly. + """ + connection = connection or get_connection(username=auth_user, + password=auth_password, + fail_silently=fail_silently) + messages = [EmailMessage(subject, message, sender, recipient) + for subject, message, sender, recipient in datatuple] + return connection.send_messages(messages) + + +def mail_admins(subject, message, fail_silently=False, connection=None): + """Sends a message to the admins, as defined by the ADMINS setting.""" + if not settings.ADMINS: + return + EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, + settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS], + connection=connection).send(fail_silently=fail_silently) + + +def mail_managers(subject, message, fail_silently=False, connection=None): + """Sends a message to the managers, as defined by the MANAGERS setting.""" + if not settings.MANAGERS: + return + EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, + settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS], + connection=connection).send(fail_silently=fail_silently) + + +class SMTPConnection(_SMTPConnection): + def __init__(self, *args, **kwds): + import warnings + warnings.warn( + 'mail.SMTPConnection is deprecated; use mail.get_connection() instead.', + DeprecationWarning + ) + super(SMTPConnection, self).__init__(*args, **kwds) diff --git a/django/core/mail/backends/__init__.py b/django/core/mail/backends/__init__.py new file mode 100644 index 0000000000..5973b499b0 --- /dev/null +++ b/django/core/mail/backends/__init__.py @@ -0,0 +1 @@ +# Mail backends shipped with Django. diff --git a/django/core/mail/backends/base.py b/django/core/mail/backends/base.py new file mode 100644 index 0000000000..9a3092849d --- /dev/null +++ b/django/core/mail/backends/base.py @@ -0,0 +1,39 @@ +"""Base email backend class.""" + +class BaseEmailBackend(object): + """ + Base class for email backend implementations. + + Subclasses must at least overwrite send_messages(). + """ + def __init__(self, fail_silently=False, **kwargs): + self.fail_silently = fail_silently + + def open(self): + """Open a network connection. + + This method can be overwritten by backend implementations to + open a network connection. + + It's up to the backend implementation to track the status of + a network connection if it's needed by the backend. + + This method can be called by applications to force a single + network connection to be used when sending mails. See the + send_messages() method of the SMTP backend for a reference + implementation. + + The default implementation does nothing. + """ + pass + + def close(self): + """Close a network connection.""" + pass + + def send_messages(self, email_messages): + """ + Sends one or more EmailMessage objects and returns the number of email + messages sent. + """ + raise NotImplementedError diff --git a/django/core/mail/backends/console.py b/django/core/mail/backends/console.py new file mode 100644 index 0000000000..fa71f3816f --- /dev/null +++ b/django/core/mail/backends/console.py @@ -0,0 +1,37 @@ +""" +Email backend that writes messages to console instead of sending them. +""" +import sys +import threading + +from django.core.mail.backends.base import BaseEmailBackend + +class EmailBackend(BaseEmailBackend): + def __init__(self, *args, **kwargs): + self.stream = kwargs.pop('stream', sys.stdout) + self._lock = threading.RLock() + super(EmailBackend, self).__init__(*args, **kwargs) + + def send_messages(self, email_messages): + """Write all messages to the stream in a thread-safe way.""" + if not email_messages: + return + self._lock.acquire() + try: + # The try-except is nested to allow for + # Python 2.4 support (Refs #12147) + try: + stream_created = self.open() + for message in email_messages: + self.stream.write('%s\n' % message.message().as_string()) + self.stream.write('-'*79) + self.stream.write('\n') + self.stream.flush() # flush after each message + if stream_created: + self.close() + except: + if not self.fail_silently: + raise + finally: + self._lock.release() + return len(email_messages) diff --git a/django/core/mail/backends/dummy.py b/django/core/mail/backends/dummy.py new file mode 100644 index 0000000000..273aa0d88e --- /dev/null +++ b/django/core/mail/backends/dummy.py @@ -0,0 +1,9 @@ +""" +Dummy email backend that does nothing. +""" + +from django.core.mail.backends.base import BaseEmailBackend + +class EmailBackend(BaseEmailBackend): + def send_messages(self, email_messages): + return len(email_messages) diff --git a/django/core/mail/backends/filebased.py b/django/core/mail/backends/filebased.py new file mode 100644 index 0000000000..3f6b99b057 --- /dev/null +++ b/django/core/mail/backends/filebased.py @@ -0,0 +1,59 @@ +"""Email backend that writes messages to a file.""" + +import datetime +import os + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.mail.backends.console import EmailBackend as ConsoleEmailBackend + +class EmailBackend(ConsoleEmailBackend): + def __init__(self, *args, **kwargs): + self._fname = None + if 'file_path' in kwargs: + self.file_path = kwargs.pop('file_path') + else: + self.file_path = getattr(settings, 'EMAIL_FILE_PATH',None) + # Make sure self.file_path is a string. + if not isinstance(self.file_path, basestring): + raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.file_path) + self.file_path = os.path.abspath(self.file_path) + # Make sure that self.file_path is an directory if it exists. + if os.path.exists(self.file_path) and not os.path.isdir(self.file_path): + raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.file_path) + # Try to create it, if it not exists. + elif not os.path.exists(self.file_path): + try: + os.makedirs(self.file_path) + except OSError, err: + raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.file_path, err)) + # Make sure that self.file_path is writable. + if not os.access(self.file_path, os.W_OK): + raise ImproperlyConfigured('Could not write to directory: %s' % self.file_path) + # Finally, call super(). + # Since we're using the console-based backend as a base, + # force the stream to be None, so we don't default to stdout + kwargs['stream'] = None + super(EmailBackend, self).__init__(*args, **kwargs) + + def _get_filename(self): + """Return a unique file name.""" + if self._fname is None: + timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + fname = "%s-%s.log" % (timestamp, abs(id(self))) + self._fname = os.path.join(self.file_path, fname) + return self._fname + + def open(self): + if self.stream is None: + self.stream = open(self._get_filename(), 'a') + return True + return False + + def close(self): + try: + if self.stream is not None: + self.stream.close() + finally: + self.stream = None + diff --git a/django/core/mail/backends/locmem.py b/django/core/mail/backends/locmem.py new file mode 100644 index 0000000000..642bfc49fb --- /dev/null +++ b/django/core/mail/backends/locmem.py @@ -0,0 +1,24 @@ +""" +Backend for test environment. +""" + +from django.core import mail +from django.core.mail.backends.base import BaseEmailBackend + +class EmailBackend(BaseEmailBackend): + """A email backend for use during test sessions. + + The test connection stores email messages in a dummy outbox, + rather than sending them out on the wire. + + The dummy outbox is accessible through the outbox instance attribute. + """ + def __init__(self, *args, **kwargs): + super(EmailBackend, self).__init__(*args, **kwargs) + if not hasattr(mail, 'outbox'): + mail.outbox = [] + + def send_messages(self, messages): + """Redirect messages to the dummy outbox""" + mail.outbox.extend(messages) + return len(messages) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py new file mode 100644 index 0000000000..63efe438d3 --- /dev/null +++ b/django/core/mail/backends/smtp.py @@ -0,0 +1,106 @@ +"""SMTP email backend class.""" + +import smtplib +import socket +import threading + +from django.conf import settings +from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail.utils import DNS_NAME + +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): + super(EmailBackend, self).__init__(fail_silently=fail_silently) + self.host = host or settings.EMAIL_HOST + self.port = port or settings.EMAIL_PORT + self.username = username or settings.EMAIL_HOST_USER + self.password = password or settings.EMAIL_HOST_PASSWORD + if use_tls is None: + self.use_tls = settings.EMAIL_USE_TLS + else: + self.use_tls = use_tls + self.connection = None + self._lock = threading.RLock() + + def open(self): + """ + Ensures we have a connection to the email server. Returns whether or + not a new connection was required (True or False). + """ + if self.connection: + # Nothing to do if the connection is already open. + return False + 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, + local_hostname=DNS_NAME.get_fqdn()) + 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 + except: + if not self.fail_silently: + raise + + def close(self): + """Closes the connection to the email server.""" + try: + try: + self.connection.quit() + except socket.sslerror: + # This happens when calling quit() on a TLS connection + # sometimes. + self.connection.close() + except: + if self.fail_silently: + return + raise + finally: + self.connection = None + + def send_messages(self, email_messages): + """ + Sends one or more EmailMessage objects and returns the number of email + messages sent. + """ + if not email_messages: + return + self._lock.acquire() + try: + new_conn_created = self.open() + if not self.connection: + # We failed silently on open(). + # Trying to send would be pointless. + return + num_sent = 0 + for message in email_messages: + sent = self._send(message) + if sent: + num_sent += 1 + if new_conn_created: + self.close() + finally: + self._lock.release() + return num_sent + + def _send(self, email_message): + """A helper method that does the actual sending.""" + if not email_message.recipients(): + return False + try: + self.connection.sendmail(email_message.from_email, + email_message.recipients(), + email_message.message().as_string()) + except: + if not self.fail_silently: + raise + return False + return True diff --git a/django/core/mail.py b/django/core/mail/message.py similarity index 59% rename from django/core/mail.py rename to django/core/mail/message.py index c305699158..14d0017311 100644 --- a/django/core/mail.py +++ b/django/core/mail/message.py @@ -1,21 +1,16 @@ -""" -Tools for sending email. -""" - import mimetypes import os -import smtplib -import socket -import time import random +import time from email import Charset, Encoders from email.MIMEText import MIMEText from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.Header import Header -from email.Utils import formatdate, parseaddr, formataddr +from email.Utils import formatdate, getaddresses, formataddr from django.conf import settings +from django.core.mail.utils import DNS_NAME from django.utils.encoding import smart_str, force_unicode # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from @@ -26,18 +21,10 @@ Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') # and cannot be guessed). DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream' -# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of -# seconds, which slows down the restart of the server. -class CachedDnsName(object): - def __str__(self): - return self.get_fqdn() - def get_fqdn(self): - if not hasattr(self, '_fqdn'): - self._fqdn = socket.getfqdn() - return self._fqdn +class BadHeaderError(ValueError): + pass -DNS_NAME = CachedDnsName() # Copied from Python standard library, with the following modifications: # * Used cached hostname for performance. @@ -66,8 +53,6 @@ def make_msgid(idstring=None): msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) return msgid -class BadHeaderError(ValueError): - pass def forbid_multi_line_headers(name, val): """Forbids multi-line headers, to prevent header injection.""" @@ -79,8 +64,7 @@ def forbid_multi_line_headers(name, val): except UnicodeEncodeError: if name.lower() in ('to', 'from', 'cc'): result = [] - for item in val.split(', '): - nm, addr = parseaddr(item) + for nm, addr in getaddresses((val,)): nm = str(Header(nm, settings.DEFAULT_CHARSET)) result.append(formataddr((nm, str(addr)))) val = ', '.join(result) @@ -91,104 +75,18 @@ def forbid_multi_line_headers(name, val): val = Header(val) return name, val + class SafeMIMEText(MIMEText): def __setitem__(self, name, val): name, val = forbid_multi_line_headers(name, val) MIMEText.__setitem__(self, name, val) + class SafeMIMEMultipart(MIMEMultipart): def __setitem__(self, name, val): name, val = forbid_multi_line_headers(name, val) MIMEMultipart.__setitem__(self, name, val) -class SMTPConnection(object): - """ - 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): - self.host = host or settings.EMAIL_HOST - self.port = port or settings.EMAIL_PORT - self.username = username or settings.EMAIL_HOST_USER - self.password = password or settings.EMAIL_HOST_PASSWORD - self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS - self.fail_silently = fail_silently - self.connection = None - - def open(self): - """ - Ensures we have a connection to the email server. Returns whether or - not a new connection was required (True or False). - """ - if self.connection: - # Nothing to do if the connection is already open. - return False - 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, - local_hostname=DNS_NAME.get_fqdn()) - 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 - except: - if not self.fail_silently: - raise - - def close(self): - """Closes the connection to the email server.""" - try: - try: - self.connection.quit() - except socket.sslerror: - # This happens when calling quit() on a TLS connection - # sometimes. - self.connection.close() - except: - if self.fail_silently: - return - raise - finally: - self.connection = None - - def send_messages(self, email_messages): - """ - Sends one or more EmailMessage objects and returns the number of email - messages sent. - """ - if not email_messages: - return - new_conn_created = self.open() - if not self.connection: - # We failed silently on open(). Trying to send would be pointless. - return - num_sent = 0 - for message in email_messages: - sent = self._send(message) - if sent: - num_sent += 1 - if new_conn_created: - self.close() - return num_sent - - def _send(self, email_message): - """A helper method that does the actual sending.""" - if not email_message.recipients(): - return False - try: - self.connection.sendmail(email_message.from_email, - email_message.recipients(), - email_message.message().as_string()) - except: - if not self.fail_silently: - raise - return False - return True class EmailMessage(object): """ @@ -199,14 +97,14 @@ class EmailMessage(object): encoding = None # None => use settings default def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, - connection=None, attachments=None, headers=None): + connection=None, attachments=None, headers=None): """ Initialize a single email message (which can be sent to multiple recipients). - All strings used to create the message can be unicode strings (or UTF-8 - bytestrings). The SafeMIMEText class will handle any necessary encoding - conversions. + All strings used to create the message can be unicode strings + (or UTF-8 bytestrings). The SafeMIMEText class will handle any + necessary encoding conversions. """ if to: assert not isinstance(to, basestring), '"to" argument must be a list or tuple' @@ -226,8 +124,9 @@ class EmailMessage(object): self.connection = connection def get_connection(self, fail_silently=False): + from django.core.mail import get_connection if not self.connection: - self.connection = SMTPConnection(fail_silently=fail_silently) + self.connection = get_connection(fail_silently=fail_silently) return self.connection def message(self): @@ -332,6 +231,7 @@ class EmailMessage(object): filename=filename) return attachment + class EmailMultiAlternatives(EmailMessage): """ A version of EmailMessage that makes it easy to send multipart/alternative @@ -371,56 +271,3 @@ class EmailMultiAlternatives(EmailMessage): for alternative in self.alternatives: msg.attach(self._create_mime_attachment(*alternative)) return msg - -def send_mail(subject, message, from_email, recipient_list, - fail_silently=False, auth_user=None, auth_password=None): - """ - Easy wrapper for sending a single message to a recipient list. All members - of the recipient list will see the other recipients in the 'To' field. - - If auth_user is None, the EMAIL_HOST_USER setting is used. - If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. - - Note: The API for this method is frozen. New code wanting to extend the - functionality should use the EmailMessage class directly. - """ - connection = SMTPConnection(username=auth_user, password=auth_password, - fail_silently=fail_silently) - return EmailMessage(subject, message, from_email, recipient_list, - connection=connection).send() - -def send_mass_mail(datatuple, fail_silently=False, auth_user=None, - auth_password=None): - """ - Given a datatuple of (subject, message, from_email, recipient_list), sends - each message to each recipient list. Returns the number of e-mails sent. - - If from_email is None, the DEFAULT_FROM_EMAIL setting is used. - If auth_user and auth_password are set, they're used to log in. - If auth_user is None, the EMAIL_HOST_USER setting is used. - If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. - - Note: The API for this method is frozen. New code wanting to extend the - functionality should use the EmailMessage class directly. - """ - connection = SMTPConnection(username=auth_user, password=auth_password, - fail_silently=fail_silently) - messages = [EmailMessage(subject, message, sender, recipient) - for subject, message, sender, recipient in datatuple] - return connection.send_messages(messages) - -def mail_admins(subject, message, fail_silently=False): - """Sends a message to the admins, as defined by the ADMINS setting.""" - if not settings.ADMINS: - return - EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, - settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS] - ).send(fail_silently=fail_silently) - -def mail_managers(subject, message, fail_silently=False): - """Sends a message to the managers, as defined by the MANAGERS setting.""" - if not settings.MANAGERS: - return - EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, - settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS] - ).send(fail_silently=fail_silently) diff --git a/django/core/mail/utils.py b/django/core/mail/utils.py new file mode 100644 index 0000000000..322a3a1b79 --- /dev/null +++ b/django/core/mail/utils.py @@ -0,0 +1,19 @@ +""" +Email message and email sending related helper functions. +""" + +import socket + + +# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of +# seconds, which slows down the restart of the server. +class CachedDnsName(object): + def __str__(self): + return self.get_fqdn() + + def get_fqdn(self): + if not hasattr(self, '_fqdn'): + self._fqdn = socket.getfqdn() + return self._fqdn + +DNS_NAME = CachedDnsName() diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 60dcf727e4..7af1a81d0a 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -299,7 +299,7 @@ class ManagementUtility(object): # subcommand if cword == 1: - print ' '.join(filter(lambda x: x.startswith(curr), subcommands)) + print ' '.join(sorted(filter(lambda x: x.startswith(curr), subcommands))) # subcommand options # special case: the 'help' subcommand has no options elif cwords[0] in subcommands and cwords[0] != 'help': @@ -328,7 +328,7 @@ class ManagementUtility(object): options = filter(lambda (x, v): x not in prev_opts, options) # filter options by current input - options = [(k, v) for k, v in options if k.startswith(curr)] + options = sorted([(k, v) for k, v in options if k.startswith(curr)]) for option in options: opt_label = option[0] # append '=' to options which require args diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index ff196494bb..f4b358887d 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -30,11 +30,8 @@ class Command(NoArgsCommand): show_traceback = options.get('traceback', False) self.style = no_style() - - if not options['database']: - dbs = connections - else: - dbs = [options['database']] + + connection = connections[options["database"]] # Import the 'management' module within each installed app, to register # dispatcher events. @@ -55,85 +52,92 @@ class Command(NoArgsCommand): if not msg.startswith('No module named') or 'management' not in msg: raise - for db in dbs: - connection = connections[db] - cursor = connection.cursor() + cursor = connection.cursor() - # Get a list of already installed *models* so that references work right. - tables = connection.introspection.table_names() - seen_models = connection.introspection.installed_models(tables) - created_models = set() - pending_references = {} + # Get a list of already installed *models* so that references work right. + tables = connection.introspection.table_names() + seen_models = connection.introspection.installed_models(tables) + created_models = set() + pending_references = {} - # Create the tables for each model - for app in models.get_apps(): - app_name = app.__name__.split('.')[-2] - model_list = models.get_models(app) - for model in model_list: - # Create the model's database table, if it doesn't already exist. - if verbosity >= 2: - print "Processing %s.%s model" % (app_name, model._meta.object_name) - if connection.introspection.table_name_converter(model._meta.db_table) in tables: - continue - sql, references = connection.creation.sql_create_model(model, self.style, seen_models) - seen_models.add(model) - created_models.add(model) - for refto, refs in references.items(): - pending_references.setdefault(refto, []).extend(refs) - if refto in seen_models: - sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references)) - sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references)) - if verbosity >= 1 and sql: - print "Creating table %s" % model._meta.db_table - for statement in sql: - cursor.execute(statement) - tables.append(connection.introspection.table_name_converter(model._meta.db_table)) + # Create the tables for each model + for app in models.get_apps(): + app_name = app.__name__.split('.')[-2] + model_list = models.get_models(app, include_auto_created=True) + for model in model_list: + # Create the model's database table, if it doesn't already exist. + if verbosity >= 2: + print "Processing %s.%s model" % (app_name, model._meta.object_name) + opts = model._meta + if (connection.introspection.table_name_converter(opts.db_table) in tables or + (opts.auto_created and + connection.introspection.table_name_converter(opts.auto_created._meta.db_table) in tables)): + continue + sql, references = connection.creation.sql_create_model(model, self.style, seen_models) + seen_models.add(model) + created_models.add(model) + for refto, refs in references.items(): + pending_references.setdefault(refto, []).extend(refs) + if refto in seen_models: + sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references)) + sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references)) + if verbosity >= 1 and sql: + print "Creating table %s" % model._meta.db_table + for statement in sql: + cursor.execute(statement) + tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - # Create the m2m tables. This must be done after all tables have been created - # to ensure that all referred tables will exist. - for app in models.get_apps(): - app_name = app.__name__.split('.')[-2] - model_list = models.get_models(app) - for model in model_list: - if model in created_models: - sql = connection.creation.sql_for_many_to_many(model, self.style) - if sql: - if verbosity >= 2: - print "Creating many-to-many tables for %s.%s model" % (app_name, model._meta.object_name) - for statement in sql: - cursor.execute(statement) - transaction.commit_unless_managed(using=db) + transaction.commit_unless_managed() - # Send the post_syncdb signal, so individual apps can do whatever they need - # to do at this point. - emit_post_sync_signal(created_models, verbosity, interactive, db) + # Send the post_syncdb signal, so individual apps can do whatever they need + # to do at this point. + emit_post_sync_signal(created_models, verbosity, interactive) - # The connection may have been closed by a syncdb handler. - cursor = connection.cursor() + # The connection may have been closed by a syncdb handler. + cursor = connection.cursor() - # Install custom SQL for the app (but only if this - # is a model we've just created) - for app in models.get_apps(): - app_name = app.__name__.split('.')[-2] - for model in models.get_models(app): - if model in created_models: - custom_sql = custom_sql_for_model(model, self.style, connection) - if custom_sql: - if verbosity >= 1: - print "Installing custom SQL for %s.%s model" % (app_name, model._meta.object_name) - try: - for sql in custom_sql: - cursor.execute(sql) - except Exception, e: - sys.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \ - (app_name, model._meta.object_name, e)) - if show_traceback: - import traceback - traceback.print_exc() - transaction.rollback_unless_managed(using=db) - else: - transaction.commit_unless_managed(using=db) + # Install custom SQL for the app (but only if this + # is a model we've just created) + for app in models.get_apps(): + app_name = app.__name__.split('.')[-2] + for model in models.get_models(app): + if model in created_models: + custom_sql = custom_sql_for_model(model, self.style) + if custom_sql: + if verbosity >= 1: + print "Installing custom SQL for %s.%s model" % (app_name, model._meta.object_name) + try: + for sql in custom_sql: + cursor.execute(sql) + except Exception, e: + sys.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \ + (app_name, model._meta.object_name, e)) + if show_traceback: + import traceback + traceback.print_exc() + transaction.rollback_unless_managed() + else: + transaction.commit_unless_managed() + else: + if verbosity >= 2: + print "No custom SQL for %s.%s model" % (app_name, model._meta.object_name) + # Install SQL indicies for all newly created models + for app in models.get_apps(): + app_name = app.__name__.split('.')[-2] + for model in models.get_models(app): + if model in created_models: + index_sql = connection.creation.sql_indexes_for_model(model, self.style) + if index_sql: + if verbosity >= 1: + print "Installing index for %s.%s model" % (app_name, model._meta.object_name) + try: + for sql in index_sql: + cursor.execute(sql) + except Exception, e: + sys.stderr.write("Failed to install index for %s.%s model: %s\n" % \ + (app_name, model._meta.object_name, e)) + transaction.rollback_unless_managed() else: if verbosity >= 2: print "No custom SQL for %s.%s model" % (app_name, model._meta.object_name) diff --git a/django/core/management/sql.py b/django/core/management/sql.py index ca229844a2..7025586693 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -28,7 +28,7 @@ def sql_create(app, style, connection): # We trim models from the current app so that the sqlreset command does not # generate invalid SQL (leaving models out of known_models is harmless, so # we can be conservative). - app_models = models.get_models(app) + app_models = models.get_models(app, include_auto_created=True) final_output = [] tables = connection.introspection.table_names() known_models = set([model for model in connection.introspection.installed_models(tables) if model not in app_models]) @@ -45,10 +45,6 @@ def sql_create(app, style, connection): # Keep track of the fact that we've created the table for this model. known_models.add(model) - # Create the many-to-many join tables. - for model in app_models: - final_output.extend(connection.creation.sql_for_many_to_many(model, style)) - # Handle references to tables that are from other apps # but don't exist physically. not_installed_models = set(pending_references.keys()) @@ -84,7 +80,7 @@ def sql_delete(app, style, connection): to_delete = set() references_to_delete = {} - app_models = models.get_models(app) + app_models = models.get_models(app, include_auto_created=True) for model in app_models: if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names: # The table exists, so it needs to be dropped @@ -99,13 +95,6 @@ def sql_delete(app, style, connection): if connection.introspection.table_name_converter(model._meta.db_table) in table_names: output.extend(connection.creation.sql_destroy_model(model, references_to_delete, style)) - # Output DROP TABLE statements for many-to-many tables. - for model in app_models: - opts = model._meta - for f in opts.local_many_to_many: - if cursor and connection.introspection.table_name_converter(f.m2m_db_table()) in table_names: - output.extend(connection.creation.sql_destroy_many_to_many(model, f, style)) - # Close database connection explicitly, in case this output is being piped # directly into a database client, to avoid locking issues. if cursor: diff --git a/django/core/management/validation.py b/django/core/management/validation.py index b971558ac7..97164d75c3 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -79,27 +79,28 @@ def get_validation_errors(outfile, app=None): rel_opts = f.rel.to._meta rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() rel_query_name = f.related_query_name() - for r in rel_opts.fields: - if r.name == rel_name: - e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - if r.name == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - for r in rel_opts.local_many_to_many: - if r.name == rel_name: - e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - if r.name == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - for r in rel_opts.get_all_related_many_to_many_objects(): - if r.get_accessor_name() == rel_name: - e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) - if r.get_accessor_name() == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) - for r in rel_opts.get_all_related_objects(): - if r.field is not f: + if not f.rel.is_hidden(): + for r in rel_opts.fields: + if r.name == rel_name: + e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + if r.name == rel_query_name: + e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.local_many_to_many: + if r.name == rel_name: + e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + if r.name == rel_query_name: + e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.get_all_related_many_to_many_objects(): if r.get_accessor_name() == rel_name: - e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) if r.get_accessor_name() == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + for r in rel_opts.get_all_related_objects(): + if r.field is not f: + if r.get_accessor_name() == rel_name: + e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + if r.get_accessor_name() == rel_query_name: + e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) seen_intermediary_signatures = [] for i, f in enumerate(opts.local_many_to_many): @@ -117,48 +118,80 @@ def get_validation_errors(outfile, app=None): if f.unique: e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) - if getattr(f.rel, 'through', None) is not None: - if hasattr(f.rel, 'through_model'): - from_model, to_model = cls, f.rel.to - if from_model == to_model and f.rel.symmetrical: - e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") - seen_from, seen_to, seen_self = False, False, 0 - for inter_field in f.rel.through_model._meta.fields: - rel_to = getattr(inter_field.rel, 'to', None) - if from_model == to_model: # relation to self - if rel_to == from_model: - seen_self += 1 - if seen_self > 2: - e.add(opts, "Intermediary model %s has more than two foreign keys to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name)) - else: - if rel_to == from_model: - if seen_from: - e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name)) - else: - seen_from = True - elif rel_to == to_model: - if seen_to: - e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name)) - else: - seen_to = True - if f.rel.through_model not in models.get_models(): - e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through)) - signature = (f.rel.to, cls, f.rel.through_model) - if signature in seen_intermediary_signatures: - e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name)) + if f.rel.through is not None and not isinstance(f.rel.through, basestring): + from_model, to_model = cls, f.rel.to + if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created: + e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") + seen_from, seen_to, seen_self = False, False, 0 + for inter_field in f.rel.through._meta.fields: + rel_to = getattr(inter_field.rel, 'to', None) + if from_model == to_model: # relation to self + if rel_to == from_model: + seen_self += 1 + if seen_self > 2: + e.add(opts, "Intermediary model %s has more than " + "two foreign keys to %s, which is ambiguous " + "and is not permitted." % ( + f.rel.through._meta.object_name, + from_model._meta.object_name + ) + ) else: - seen_intermediary_signatures.append(signature) - seen_related_fk, seen_this_fk = False, False - for field in f.rel.through_model._meta.fields: - if field.rel: - if not seen_related_fk and field.rel.to == f.rel.to: - seen_related_fk = True - elif field.rel.to == cls: - seen_this_fk = True - if not seen_related_fk or not seen_this_fk: - e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name)) + if rel_to == from_model: + if seen_from: + e.add(opts, "Intermediary model %s has more " + "than one foreign key to %s, which is " + "ambiguous and is not permitted." % ( + f.rel.through._meta.object_name, + from_model._meta.object_name + ) + ) + else: + seen_from = True + elif rel_to == to_model: + if seen_to: + e.add(opts, "Intermediary model %s has more " + "than one foreign key to %s, which is " + "ambiguous and is not permitted." % ( + f.rel.through._meta.object_name, + rel_to._meta.object_name + ) + ) + else: + seen_to = True + if f.rel.through not in models.get_models(include_auto_created=True): + e.add(opts, "'%s' specifies an m2m relation through model " + "%s, which has not been installed." % (f.name, f.rel.through) + ) + signature = (f.rel.to, cls, f.rel.through) + if signature in seen_intermediary_signatures: + e.add(opts, "The model %s has two manually-defined m2m " + "relations through the model %s, which is not " + "permitted. Please consider using an extra field on " + "your intermediary model instead." % ( + cls._meta.object_name, + f.rel.through._meta.object_name + ) + ) else: - e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) + seen_intermediary_signatures.append(signature) + seen_related_fk, seen_this_fk = False, False + for field in f.rel.through._meta.fields: + if field.rel: + if not seen_related_fk and field.rel.to == f.rel.to: + seen_related_fk = True + elif field.rel.to == cls: + seen_this_fk = True + if not seen_related_fk or not seen_this_fk: + e.add(opts, "'%s' has a manually-defined m2m relation " + "through model %s, which does not have foreign keys " + "to %s and %s" % (f.name, f.rel.through._meta.object_name, + f.rel.to._meta.object_name, cls._meta.object_name) + ) + elif isinstance(f.rel.through, basestring): + e.add(opts, "'%s' specifies an m2m relation through model %s, " + "which has not been installed" % (f.name, f.rel.through) + ) rel_opts = f.rel.to._meta rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index b672e4efc3..7b77804009 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -56,7 +56,7 @@ class Serializer(base.Serializer): self._current[field.name] = smart_unicode(related, strings_only=True) def handle_m2m_field(self, obj, field): - if field.creates_table: + if field.rel.through._meta.auto_created: self._current[field.name] = [smart_unicode(related._get_pk_val(), strings_only=True) for related in getattr(obj, field.name).iterator()] diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 2d74fe28f3..4cde0b039d 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -98,7 +98,7 @@ class Serializer(base.Serializer): serialized as references to the object's PK (i.e. the related *data* is not dumped, just the relation). """ - if field.creates_table: + if field.rel.through._meta.auto_created: self._start_relational_field(field) for relobj in getattr(obj, field.name).iterator(): self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())}) @@ -233,4 +233,3 @@ def getInnerText(node): else: pass return u"".join(inner_text) - diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index ae9ed3bcea..a924afeaf8 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -10,6 +10,7 @@ a string) and returns a tuple in this format: import re from django.http import Http404 +from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.utils.datastructures import MultiValueDict from django.utils.encoding import iri_to_uri, force_unicode, smart_str @@ -32,6 +33,9 @@ _callable_cache = {} # Maps view and url pattern names to their view functions. # be empty. _prefixes = {} +# Overridden URLconfs for each thread are stored here. +_urlconfs = {} + class Resolver404(Http404): pass @@ -300,9 +304,13 @@ class RegexURLResolver(object): "arguments '%s' not found." % (lookup_view_s, args, kwargs)) def resolve(path, urlconf=None): + if urlconf is None: + urlconf = get_urlconf() return get_resolver(urlconf).resolve(path) def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + if urlconf is None: + urlconf = get_urlconf() resolver = get_resolver(urlconf) args = args or [] kwargs = kwargs or {} @@ -370,3 +378,26 @@ def get_script_prefix(): instance is normally going to be a lot cleaner). """ return _prefixes.get(currentThread(), u'/') + +def set_urlconf(urlconf_name): + """ + Sets the URLconf for the current thread (overriding the default one in + settings). Set to None to revert back to the default. + """ + thread = currentThread() + if urlconf_name: + _urlconfs[thread] = urlconf_name + else: + # faster than wrapping in a try/except + if thread in _urlconfs: + del _urlconfs[thread] + +def get_urlconf(default=None): + """ + Returns the root URLconf to use for the current thread if it has been + changed from the default one. + """ + thread = currentThread() + if thread in _urlconfs: + return _urlconfs[thread] + return default diff --git a/django/db/models/base.py b/django/db/models/base.py index 82a0f9862b..0887abc967 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -3,11 +3,6 @@ import types import sys import os from itertools import izip -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback. - import django.db.models.manager # Imported to register signal handler. from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError from django.db.models.fields import AutoField, FieldDoesNotExist @@ -22,7 +17,6 @@ from django.utils.functional import curry from django.utils.encoding import smart_str, force_unicode, smart_unicode from django.conf import settings - class ModelBase(type): """ Metaclass for all models. @@ -236,7 +230,6 @@ class ModelBase(type): signals.class_prepared.send(sender=cls) - class Model(object): __metaclass__ = ModelBase _deferred = False @@ -300,7 +293,14 @@ class Model(object): if rel_obj is None and field.null: val = None else: - val = kwargs.pop(field.attname, field.get_default()) + try: + val = kwargs.pop(field.attname) + except KeyError: + # This is done with an exception rather than the + # default argument on pop because we don't want + # get_default() to be evaluated, and then not used. + # Refs #12057. + val = field.get_default() else: val = field.get_default() if is_related_object: @@ -352,21 +352,30 @@ class Model(object): only module-level classes can be pickled by the default path. """ data = self.__dict__ - if not self._deferred: - return (self.__class__, (), data) + model = self.__class__ + # The obvious thing to do here is to invoke super().__reduce__() + # for the non-deferred case. Don't do that. + # On Python 2.4, there is something wierd with __reduce__, + # and as a result, the super call will cause an infinite recursion. + # See #10547 and #12121. defers = [] pk_val = None - for field in self._meta.fields: - if isinstance(self.__class__.__dict__.get(field.attname), - DeferredAttribute): - defers.append(field.attname) - if pk_val is None: - # The pk_val and model values are the same for all - # DeferredAttribute classes, so we only need to do this - # once. - obj = self.__class__.__dict__[field.attname] - model = obj.model_ref() - return (model_unpickle, (model, defers), data) + if self._deferred: + from django.db.models.query_utils import deferred_class_factory + factory = deferred_class_factory + for field in self._meta.fields: + if isinstance(self.__class__.__dict__.get(field.attname), + DeferredAttribute): + defers.append(field.attname) + if pk_val is None: + # The pk_val and model values are the same for all + # DeferredAttribute classes, so we only need to do this + # once. + obj = self.__class__.__dict__[field.attname] + model = obj.model_ref() + else: + factory = simple_class_factory + return (model_unpickle, (model, defers, factory), data) def _get_pk_val(self, meta=None): if not meta: @@ -430,7 +439,7 @@ class Model(object): else: meta = cls._meta - if origin: + if origin and not meta.auto_created: signals.pre_save.send(sender=origin, instance=self, raw=raw) # If we are in a raw save, save the object exactly as presented. @@ -469,7 +478,7 @@ class Model(object): if pk_set: # Determine whether a record with the primary key already exists. if (force_update or (not force_insert and - manager.using(using).filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())): + manager.using(using).filter(pk=pk_val).exists())): # It does already exist, so do an UPDATE. if force_update or non_pks: values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] @@ -505,7 +514,7 @@ class Model(object): setattr(self, meta.pk.attname, result) transaction.commit_unless_managed(using=using) - if origin: + if origin and not meta.auto_created: signals.post_save.send(sender=origin, instance=self, created=(not record_exists), raw=raw) @@ -542,7 +551,12 @@ class Model(object): rel_descriptor = cls.__dict__[rel_opts_name] break else: - raise AssertionError("Should never get here.") + # in the case of a hidden fkey just skip it, it'll get + # processed as an m2m + if not related.field.rel.is_hidden(): + raise AssertionError("Should never get here.") + else: + continue delete_qs = rel_descriptor.delete_manager(self).all() for sub_obj in delete_qs: sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) @@ -653,12 +667,20 @@ def get_absolute_url(opts, func, self, *args, **kwargs): class Empty(object): pass -def model_unpickle(model, attrs): +def simple_class_factory(model, attrs): + """Used to unpickle Models without deferred fields. + + We need to do this the hard way, rather than just using + the default __reduce__ implementation, because of a + __deepcopy__ problem in Python 2.4 + """ + return model + +def model_unpickle(model, attrs, factory): """ Used to unpickle Model subclasses with deferred fields. """ - from django.db.models.query_utils import deferred_class_factory - cls = deferred_class_factory(model, attrs) + cls = factory(model, attrs) return cls.__new__(cls) model_unpickle.__safe_for_unpickle__ = True diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 1db4156268..ce2813481a 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -58,6 +58,10 @@ def add_lazy_relation(cls, field, relation, operation): # If we can't split, assume a model in current app app_label = cls._meta.app_label model_name = relation + except AttributeError: + # If it doesn't have a split it's actually a model class + app_label = relation._meta.app_label + model_name = relation._meta.object_name # Try to look up the related model, and if it's already loaded resolve the # string right away. If get_model returns None, it means that the related @@ -96,7 +100,7 @@ class RelatedField(object): self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()} other = self.rel.to - if isinstance(other, basestring): + if isinstance(other, basestring) or other._meta.pk is None: def resolve_related_class(field, model, cls): field.rel.to = model field.do_related_class(model, cls) @@ -401,22 +405,22 @@ class ForeignRelatedObjectsDescriptor(object): return manager -def create_many_related_manager(superclass, through=False): +def create_many_related_manager(superclass, rel=False): """Creates a manager that subclasses 'superclass' (which is a Manager) and adds behavior for many-to-many related objects.""" + through = rel.through class ManyRelatedManager(superclass): def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, - join_table=None, source_col_name=None, target_col_name=None): + join_table=None, source_field_name=None, target_field_name=None): super(ManyRelatedManager, self).__init__() self.core_filters = core_filters self.model = model self.symmetrical = symmetrical self.instance = instance - self.join_table = join_table - self.source_col_name = source_col_name - self.target_col_name = target_col_name + self.source_field_name = source_field_name + self.target_field_name = target_field_name self.through = through - self._pk_val = self.instance._get_pk_val() + self._pk_val = self.instance.pk if self._pk_val is None: raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) @@ -425,36 +429,37 @@ def create_many_related_manager(superclass, through=False): # If the ManyToMany relation has an intermediary model, # the add and remove methods do not exist. - if through is None: + if rel.through._meta.auto_created: def add(self, *objs): - self._add_items(self.source_col_name, self.target_col_name, *objs) + self._add_items(self.source_field_name, self.target_field_name, *objs) # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table if self.symmetrical: - self._add_items(self.target_col_name, self.source_col_name, *objs) + self._add_items(self.target_field_name, self.source_field_name, *objs) add.alters_data = True def remove(self, *objs): - self._remove_items(self.source_col_name, self.target_col_name, *objs) + self._remove_items(self.source_field_name, self.target_field_name, *objs) # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table if self.symmetrical: - self._remove_items(self.target_col_name, self.source_col_name, *objs) + self._remove_items(self.target_field_name, self.source_field_name, *objs) remove.alters_data = True def clear(self): - self._clear_items(self.source_col_name) + self._clear_items(self.source_field_name) # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table if self.symmetrical: - self._clear_items(self.target_col_name) + self._clear_items(self.target_field_name) clear.alters_data = True def create(self, **kwargs): # This check needs to be done here, since we can't later remove this # from the method lookup table, as we do with add and remove. - if through is not None: - raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + if not rel.through._meta.auto_created: + opts = through._meta + raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) new_obj = super(ManyRelatedManager, self).create(**kwargs) self.add(new_obj) return new_obj @@ -470,43 +475,38 @@ def create_many_related_manager(superclass, through=False): return obj, created get_or_create.alters_data = True - def _add_items(self, source_col_name, target_col_name, *objs): + def _add_items(self, source_field_name, target_field_name, *objs): # join_table: name of the m2m link table - # source_col_name: the PK colname in join_table for the source object - # target_col_name: the PK colname in join_table for the target object + # source_field_name: the PK fieldname in join_table for the source object + # target_col_name: the PK fieldname in join_table for the target object # *objs - objects to add. Either object instances, or primary keys of object instances. # If there aren't any objects, there is nothing to do. + from django.db.models import Model if objs: - from django.db.models.base import Model - # Check that all the objects are of the right type new_ids = set() for obj in objs: if isinstance(obj, self.model): - new_ids.add(obj._get_pk_val()) + new_ids.add(obj.pk) elif isinstance(obj, Model): raise TypeError, "'%s' instance expected" % self.model._meta.object_name else: new_ids.add(obj) - # Add the newly created or already existing objects to the join table. - # First find out which items are already added, to avoid adding them twice - cursor = connection.cursor() - cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (target_col_name, self.join_table, source_col_name, - target_col_name, ",".join(['%s'] * len(new_ids))), - [self._pk_val] + list(new_ids)) - existing_ids = set([row[0] for row in cursor.fetchall()]) + vals = self.through._default_manager.values_list(target_field_name, flat=True) + vals = vals.filter(**{ + source_field_name: self._pk_val, + '%s__in' % target_field_name: new_ids, + }) + vals = set(vals) # Add the ones that aren't there already - for obj_id in (new_ids - existing_ids): - cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (self.join_table, source_col_name, target_col_name), - [self._pk_val, obj_id]) - # FIXME, once this isn't in related.py it should conditionally - # use the right DB. - transaction.commit_unless_managed(using=DEFAULT_DB_ALIAS) + for obj_id in (new_ids - vals): + self.through._default_manager.create(**{ + '%s_id' % source_field_name: self._pk_val, + '%s_id' % target_field_name: obj_id, + }) - def _remove_items(self, source_col_name, target_col_name, *objs): + def _remove_items(self, source_field_name, target_field_name, *objs): # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object # *objs - objects to remove @@ -517,26 +517,20 @@ def create_many_related_manager(superclass, through=False): old_ids = set() for obj in objs: if isinstance(obj, self.model): - old_ids.add(obj._get_pk_val()) + old_ids.add(obj.pk) else: old_ids.add(obj) # Remove the specified objects from the join table - cursor = connection.cursor() - cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (self.join_table, source_col_name, - target_col_name, ",".join(['%s'] * len(old_ids))), - [self._pk_val] + list(old_ids)) - # TODO - transaction.commit_unless_managed(using=DEFAULT_DB_ALIAS) + self.through._default_manager.filter(**{ + source_field_name: self._pk_val, + '%s__in' % target_field_name: old_ids + }).delete() - def _clear_items(self, source_col_name): + def _clear_items(self, source_field_name): # source_col_name: the PK colname in join_table for the source object - cursor = connection.cursor() - cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ - (self.join_table, source_col_name), - [self._pk_val]) - # TODO - transaction.commit_unless_managed(using=DEFAULT_DB_ALIAS) + self.through._default_manager.filter(**{ + source_field_name: self._pk_val + }).delete() return ManyRelatedManager @@ -558,17 +552,15 @@ class ManyRelatedObjectsDescriptor(object): # model's default manager. rel_model = self.related.model superclass = rel_model._default_manager.__class__ - RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) + RelatedManager = create_many_related_manager(superclass, self.related.field.rel) - qn = connection.ops.quote_name manager = RelatedManager( model=rel_model, core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()}, instance=instance, symmetrical=False, - join_table=qn(self.related.field.m2m_db_table()), - source_col_name=qn(self.related.field.m2m_reverse_name()), - target_col_name=qn(self.related.field.m2m_column_name()) + source_field_name=self.related.field.m2m_reverse_field_name(), + target_field_name=self.related.field.m2m_field_name() ) return manager @@ -577,9 +569,9 @@ class ManyRelatedObjectsDescriptor(object): if instance is None: raise AttributeError, "Manager must be accessed via instance" - through = getattr(self.related.field.rel, 'through', None) - if through is not None: - raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + if not self.related.field.rel.through._meta.auto_created: + opts = self.related.field.rel.through._meta + raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) manager = self.__get__(instance) manager.clear() @@ -595,6 +587,13 @@ class ReverseManyRelatedObjectsDescriptor(object): def __init__(self, m2m_field): self.field = m2m_field + def _through(self): + # through is provided so that you have easy access to the through + # model (Book.authors.through) for inlines, etc. This is done as + # a property to ensure that the fully resolved value is returned. + return self.field.rel.through + through = property(_through) + def __get__(self, instance, instance_type=None): if instance is None: return self @@ -603,17 +602,15 @@ class ReverseManyRelatedObjectsDescriptor(object): # model's default manager. rel_model=self.field.rel.to superclass = rel_model._default_manager.__class__ - RelatedManager = create_many_related_manager(superclass, self.field.rel.through) + RelatedManager = create_many_related_manager(superclass, self.field.rel) - qn = connection.ops.quote_name manager = RelatedManager( model=rel_model, core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()}, instance=instance, symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)), - join_table=qn(self.field.m2m_db_table()), - source_col_name=qn(self.field.m2m_column_name()), - target_col_name=qn(self.field.m2m_reverse_name()) + source_field_name=self.field.m2m_field_name(), + target_field_name=self.field.m2m_reverse_field_name() ) return manager @@ -622,9 +619,9 @@ class ReverseManyRelatedObjectsDescriptor(object): if instance is None: raise AttributeError, "Manager must be accessed via instance" - through = getattr(self.field.rel, 'through', None) - if through is not None: - raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + if not self.field.rel.through._meta.auto_created: + opts = self.field.rel.through._meta + raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) manager = self.__get__(instance) manager.clear() @@ -646,6 +643,10 @@ class ManyToOneRel(object): self.multiple = True self.parent_link = parent_link + def is_hidden(self): + "Should the related object be hidden?" + return self.related_name and self.related_name[-1] == '+' + def get_related_field(self): """ Returns the Field in the 'to' object to which this relationship is @@ -677,6 +678,10 @@ class ManyToManyRel(object): self.multiple = True self.through = through + def is_hidden(self): + "Should the related object be hidden?" + return self.related_name and self.related_name[-1] == '+' + def get_related_field(self): """ Returns the field in the to' object to which this relationship is tied @@ -694,7 +699,10 @@ class ForeignKey(RelatedField, Field): assert isinstance(to, basestring), "%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: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) - to_field = to_field or to._meta.pk.name + # For backwards compatibility purposes, we need to *try* and set + # the to_field during FK construction. It won't be guaranteed to + # be correct until contribute_to_class is called. Refs #12190. + to_field = to_field or (to._meta.pk and to._meta.pk.name) kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = rel_class(to, to_field, @@ -748,7 +756,12 @@ class ForeignKey(RelatedField, Field): cls._meta.duplicate_targets[self.column] = (target, "o2m") def contribute_to_related_class(self, cls, related): - setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) + # Internal FK's - i.e., those with a related name ending with '+' - + # don't get a related descriptor. + if not self.rel.is_hidden(): + setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) + if self.rel.field_name is None: + self.rel.field_name = cls._meta.pk.name def formfield(self, **kwargs): defaults = { @@ -795,6 +808,45 @@ class OneToOneField(ForeignKey): return None return super(OneToOneField, self).formfield(**kwargs) +def create_many_to_many_intermediary_model(field, klass): + from django.db import models + managed = True + if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: + to = field.rel.to + to_model = field.rel.to + def set_managed(field, model, cls): + field.rel.through._meta.managed = model._meta.managed or cls._meta.managed + add_lazy_relation(klass, field, to_model, set_managed) + elif isinstance(field.rel.to, basestring): + to = klass._meta.object_name + to_model = klass + managed = klass._meta.managed + else: + to = field.rel.to._meta.object_name + to_model = field.rel.to + managed = klass._meta.managed or to_model._meta.managed + name = '%s_%s' % (klass._meta.object_name, field.name) + if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or field.rel.to == klass._meta.object_name: + from_ = 'from_%s' % to.lower() + to = 'to_%s' % to.lower() + else: + from_ = klass._meta.object_name.lower() + to = to.lower() + meta = type('Meta', (object,), { + 'db_table': field._get_m2m_db_table(klass._meta), + 'managed': managed, + 'auto_created': klass, + 'app_label': klass._meta.app_label, + 'unique_together': (from_, to) + }) + # Construct and return the new class. + return type(name, (models.Model,), { + 'Meta': meta, + '__module__': klass.__module__, + from_: models.ForeignKey(klass, related_name='%s+' % name), + to: models.ForeignKey(to_model, related_name='%s+' % name) + }) + class ManyToManyField(RelatedField, Field): def __init__(self, to, **kwargs): try: @@ -811,10 +863,7 @@ class ManyToManyField(RelatedField, Field): self.db_table = kwargs.pop('db_table', None) if kwargs['rel'].through is not None: - self.creates_table = False assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." - else: - self.creates_table = True Field.__init__(self, **kwargs) @@ -827,62 +876,45 @@ class ManyToManyField(RelatedField, Field): def _get_m2m_db_table(self, opts): "Function that can be curried to provide the m2m table name for this relation" if self.rel.through is not None: - return self.rel.through_model._meta.db_table + return self.rel.through._meta.db_table elif self.db_table: return self.db_table else: return util.truncate_name('%s_%s' % (opts.db_table, self.name), connection.ops.max_name_length()) - def _get_m2m_column_name(self, related): + def _get_m2m_attr(self, related, attr): "Function that can be curried to provide the source column name for the m2m table" - try: - return self._m2m_column_name_cache - except: - if self.rel.through is not None: - for f in self.rel.through_model._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.model: - self._m2m_column_name_cache = f.column - break - # If this is an m2m relation to self, avoid the inevitable name clash - elif related.model == related.parent_model: - self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id' - else: - self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id' + cache_attr = '_m2m_%s_cache' % attr + if hasattr(self, cache_attr): + return getattr(self, cache_attr) + for f in self.rel.through._meta.fields: + if hasattr(f,'rel') and f.rel and f.rel.to == related.model: + setattr(self, cache_attr, getattr(f, attr)) + return getattr(self, cache_attr) - # Return the newly cached value - return self._m2m_column_name_cache - - def _get_m2m_reverse_name(self, related): + def _get_m2m_reverse_attr(self, related, attr): "Function that can be curried to provide the related column name for the m2m table" - try: - return self._m2m_reverse_name_cache - except: - if self.rel.through is not None: - found = False - for f in self.rel.through_model._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: - if related.model == related.parent_model: - # If this is an m2m-intermediate to self, - # the first foreign key you find will be - # the source column. Keep searching for - # the second foreign key. - if found: - self._m2m_reverse_name_cache = f.column - break - else: - found = True - else: - self._m2m_reverse_name_cache = f.column - break - # If this is an m2m relation to self, avoid the inevitable name clash - elif related.model == related.parent_model: - self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id' - else: - self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id' - - # Return the newly cached value - return self._m2m_reverse_name_cache + cache_attr = '_m2m_reverse_%s_cache' % attr + if hasattr(self, cache_attr): + return getattr(self, cache_attr) + found = False + for f in self.rel.through._meta.fields: + if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: + if related.model == related.parent_model: + # If this is an m2m-intermediate to self, + # the first foreign key you find will be + # the source column. Keep searching for + # the second foreign key. + if found: + setattr(self, cache_attr, getattr(f, attr)) + break + else: + found = True + else: + setattr(self, cache_attr, getattr(f, attr)) + break + return getattr(self, cache_attr) def isValidIDList(self, field_data, all_data): "Validates that the value is a valid list of foreign keys" @@ -924,10 +956,17 @@ class ManyToManyField(RelatedField, Field): # specify *what* on my non-reversible relation?!"), so we set it up # automatically. The funky name reduces the chance of an accidental # clash. - if self.rel.symmetrical and self.rel.to == "self" and self.rel.related_name is None: + if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name): self.rel.related_name = "%s_rel_+" % name super(ManyToManyField, self).contribute_to_class(cls, name) + + # The intermediate m2m model is not auto created if: + # 1) There is a manually specified intermediate, or + # 2) The class owning the m2m field is abstract. + if not self.rel.through and not cls._meta.abstract: + self.rel.through = create_many_to_many_intermediary_model(self, cls) + # Add the descriptor for the m2m relation setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) @@ -938,11 +977,8 @@ class ManyToManyField(RelatedField, Field): # work correctly. if isinstance(self.rel.through, basestring): def resolve_through_model(field, model, cls): - field.rel.through_model = model + field.rel.through = model add_lazy_relation(cls, self, self.rel.through, resolve_through_model) - elif self.rel.through: - self.rel.through_model = self.rel.through - self.rel.through = self.rel.through._meta.object_name if isinstance(self.rel.to, basestring): target = self.rel.to @@ -951,15 +987,17 @@ class ManyToManyField(RelatedField, Field): cls._meta.duplicate_targets[self.column] = (target, "m2m") def contribute_to_related_class(self, cls, related): - # m2m relations to self do not have a ManyRelatedObjectsDescriptor, - # as it would be redundant - unless the field is non-symmetrical. - if related.model != related.parent_model or not self.rel.symmetrical: - # Add the descriptor for the m2m relation + # Internal M2Ms (i.e., those with a related name ending with '+') + # don't get a related descriptor. + if not self.rel.is_hidden(): setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related)) # Set up the accessors for the column names on the m2m table - self.m2m_column_name = curry(self._get_m2m_column_name, related) - self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related) + self.m2m_column_name = curry(self._get_m2m_attr, related, 'column') + self.m2m_reverse_name = curry(self._get_m2m_reverse_attr, related, 'column') + + self.m2m_field_name = curry(self._get_m2m_attr, related, 'name') + self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name') def set_attributes_from_rel(self): pass diff --git a/django/db/models/loading.py b/django/db/models/loading.py index e07aab4efe..4ab1d5005a 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -131,19 +131,25 @@ class AppCache(object): self._populate() return self.app_errors - def get_models(self, app_mod=None): + def get_models(self, app_mod=None, include_auto_created=False): """ Given a module containing models, returns a list of the models. Otherwise returns a list of all installed models. + + By default, auto-created models (i.e., m2m models without an + explicit intermediate table) are not included. However, if you + specify include_auto_created=True, they will be. """ self._populate() if app_mod: - return self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values() + model_list = self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values() else: model_list = [] for app_entry in self.app_models.itervalues(): model_list.extend(app_entry.values()) - return model_list + if not include_auto_created: + return filter(lambda o: not o._meta.auto_created, model_list) + return model_list def get_model(self, app_label, model_name, seed_cache=True): """ diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 18ed1c161f..d752519f5c 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,5 +1,4 @@ import copy - from django.db.models.query import QuerySet, EmptyQuerySet, insert_query from django.db.models import signals from django.db.models.fields import FieldDoesNotExist @@ -176,6 +175,9 @@ class Manager(object): def using(self, *args, **kwargs): return self.get_query_set().using(*args, **kwargs) + def exists(self, *args, **kwargs): + return self.get_query_set().exists(*args, **kwargs) + def _insert(self, values, **kwargs): return insert_query(self.model, values, **kwargs) diff --git a/django/db/models/options.py b/django/db/models/options.py index 82550169db..c82da3a1cf 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy', 'using') + 'abstract', 'managed', 'proxy', 'using', 'auto_created') class Options(object): def __init__(self, meta, app_label=None): @@ -48,6 +48,7 @@ class Options(object): self.parents = SortedDict() self.duplicate_targets = {} self.using = None + self.auto_created = False # To handle various inheritance situations, we need to track where # managers came from (concrete or abstract base classes). diff --git a/django/db/models/query.py b/django/db/models/query.py index a70472d6b3..9ef9a93284 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -2,11 +2,6 @@ The main QuerySet implementation. This provides the public API for the ORM. """ -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - from copy import deepcopy from django.db import connections, transaction, IntegrityError, DEFAULT_DB_ALIAS @@ -15,7 +10,6 @@ from django.db.models.fields import DateField from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory from django.db.models import signals, sql - # Used to control how many objects are worked with at once in some cases (e.g. # when deleting objects). CHUNK_SIZE = 100 @@ -453,6 +447,11 @@ class QuerySet(object): return query.execute_sql(None) _update.alters_data = True + def exists(self): + if self._result_cache is None: + return self.query.has_results() + return bool(self._result_cache) + ################################################## # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # ################################################## @@ -1086,7 +1085,8 @@ def delete_objects(seen_objs, using): # Pre-notify all instances to be deleted. for pk_val, instance in items: - signals.pre_delete.send(sender=cls, instance=instance) + if not cls._meta.auto_created: + signals.pre_delete.send(sender=cls, instance=instance) pk_list = [pk for pk,instance in items] del_query = connection.ops.query_class(sql.Query, sql.DeleteQuery)(cls, connection) @@ -1120,7 +1120,8 @@ def delete_objects(seen_objs, using): if field.rel and field.null and field.rel.to in seen_objs: setattr(instance, field.attname, None) - signals.post_delete.send(sender=cls, instance=instance) + if not cls._meta.auto_created: + signals.post_delete.send(sender=cls, instance=instance) setattr(instance, cls._meta.pk.attname, None) if forced_managed: diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index dce7e5a4a1..123fa41dc1 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -8,7 +8,6 @@ all about the internals of models in order to get the information it needs. """ from copy import deepcopy - from django.utils.tree import Node from django.utils.datastructures import SortedDict from django.utils.encoding import force_unicode @@ -24,11 +23,6 @@ from django.core.exceptions import FieldError from datastructures import EmptyResultSet, Empty, MultiJoin from constants import * -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - __all__ = ['Query'] class Query(object): @@ -386,6 +380,16 @@ class Query(object): return number + def has_results(self): + q = self.clone() + q.add_extra({'a': 1}, None, None, None, None, None) + q.add_fields(()) + q.set_extra_mask(('a',)) + q.set_aggregate_mask(()) + q.clear_ordering() + q.set_limits(high=1) + return bool(q.execute_sql(SINGLE)) + def as_sql(self, with_limits=True, with_col_aliases=False): """ Creates the SQL for this query. Returns the SQL string and list of diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 71654e5be9..02dec73bd4 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -421,7 +421,7 @@ class DateQuery(Query): self.select = [select] self.select_fields = [None] self.select_related = False # See #7097. - self.extra = {} + self.set_extra_mask([]) self.distinct = True self.order_by = order == 'ASC' and [1] or [-1] diff --git a/django/forms/models.py b/django/forms/models.py index b3b472384d..98b63b39d5 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -319,9 +319,7 @@ class BaseModelForm(BaseForm): if self.instance.pk is not None: qs = qs.exclude(pk=self.instance.pk) - # This cute trick with extra/values is the most efficient way to - # tell if a particular query returns any results. - if qs.extra(select={'a': 1}).values('a').order_by(): + if qs.exists(): if len(unique_check) == 1: self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)]) else: @@ -354,9 +352,7 @@ class BaseModelForm(BaseForm): if self.instance.pk is not None: qs = qs.exclude(pk=self.instance.pk) - # This cute trick with extra/values is the most efficient way to - # tell if a particular query returns any results. - if qs.extra(select={'a': 1}).values('a').order_by(): + if qs.exists(): self._errors[field] = ErrorList([ self.date_error_message(lookup_type, field, unique_for) ]) @@ -476,6 +472,7 @@ class BaseModelFormSet(BaseFormSet): pk_field = self.model._meta.pk pk = pk_field.get_db_prep_lookup('exact', pk, connection=self.get_queryset().query.connection) + pk = pk_field.get_db_prep_lookup('exact', pk) if isinstance(pk, list): pk = pk[0] kwargs['instance'] = self._existing_object(pk) @@ -710,7 +707,7 @@ class BaseInlineFormSet(BaseModelFormSet): save_as_new=False, prefix=None): from django.db.models.fields.related import RelatedObject if instance is None: - self.instance = self.model() + self.instance = self.fk.rel.to() else: self.instance = instance self.save_as_new = save_as_new diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py new file mode 100644 index 0000000000..9ca727fca9 --- /dev/null +++ b/django/middleware/csrf.py @@ -0,0 +1,265 @@ +""" +Cross Site Request Forgery Middleware. + +This module provides a middleware that implements protection +against request forgeries from other sites. +""" + +import itertools +import re +import random + +from django.conf import settings +from django.core.urlresolvers import get_callable +from django.utils.cache import patch_vary_headers +from django.utils.hashcompat import md5_constructor +from django.utils.safestring import mark_safe + +_POST_FORM_RE = \ + re.compile(r'(]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) + +_HTML_TYPES = ('text/html', 'application/xhtml+xml') + +# Use the system (hardware-based) random number generator if it exists. +if hasattr(random, 'SystemRandom'): + randrange = random.SystemRandom().randrange +else: + randrange = random.randrange +_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63 + +def _get_failure_view(): + """ + Returns the view to be used for CSRF rejections + """ + return get_callable(settings.CSRF_FAILURE_VIEW) + +def _get_new_csrf_key(): + return md5_constructor("%s%s" + % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() + +def _make_legacy_session_token(session_id): + return md5_constructor(settings.SECRET_KEY + session_id).hexdigest() + +def get_token(request): + """ + Returns the the CSRF token required for a POST form. + + A side effect of calling this function is to make the the csrf_protect + decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' + header to the outgoing response. For this reason, you may need to use this + function lazily, as is done by the csrf context processor. + """ + request.META["CSRF_COOKIE_USED"] = True + return request.META.get("CSRF_COOKIE", None) + +class CsrfViewMiddleware(object): + """ + Middleware that requires a present and correct csrfmiddlewaretoken + for POST requests that have a CSRF cookie, and sets an outgoing + CSRF cookie. + + This middleware should be used in conjunction with the csrf_token template + tag. + """ + def process_view(self, request, callback, callback_args, callback_kwargs): + if getattr(callback, 'csrf_exempt', False): + return None + + if getattr(request, 'csrf_processing_done', False): + return None + + reject = lambda s: _get_failure_view()(request, reason=s) + def accept(): + # Avoid checking the request twice by adding a custom attribute to + # request. This will be relevant when both decorator and middleware + # are used. + request.csrf_processing_done = True + return None + + # If the user doesn't have a CSRF cookie, generate one and store it in the + # request, so it's available to the view. We'll store it in a cookie when + # we reach the response. + try: + request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME] + cookie_is_new = False + except KeyError: + # No cookie, so create one. This will be sent with the next + # response. + request.META["CSRF_COOKIE"] = _get_new_csrf_key() + # Set a flag to allow us to fall back and allow the session id in + # place of a CSRF cookie for this request only. + cookie_is_new = True + + if request.method == 'POST': + if getattr(request, '_dont_enforce_csrf_checks', False): + # Mechanism to turn off CSRF checks for test suite. It comes after + # the creation of CSRF cookies, so that everything else continues to + # work exactly the same (e.g. cookies are sent etc), but before the + # any branches that call reject() + return accept() + + if request.is_ajax(): + # .is_ajax() is based on the presence of X-Requested-With. In + # the context of a browser, this can only be sent if using + # XmlHttpRequest. Browsers implement careful policies for + # XmlHttpRequest: + # + # * Normally, only same-domain requests are allowed. + # + # * Some browsers (e.g. Firefox 3.5 and later) relax this + # carefully: + # + # * if it is a 'simple' GET or POST request (which can + # include no custom headers), it is allowed to be cross + # domain. These requests will not be recognized as AJAX. + # + # * if a 'preflight' check with the server confirms that the + # server is expecting and allows the request, cross domain + # requests even with custom headers are allowed. These + # requests will be recognized as AJAX, but can only get + # through when the developer has specifically opted in to + # allowing the cross-domain POST request. + # + # So in all cases, it is safe to allow these requests through. + return accept() + + if request.is_secure(): + # Strict referer checking for HTTPS + referer = request.META.get('HTTP_REFERER') + if referer is None: + return reject("Referer checking failed - no Referer.") + + # The following check ensures that the referer is HTTPS, + # the domains match and the ports match. This might be too strict. + good_referer = 'https://%s/' % request.get_host() + if not referer.startswith(good_referer): + return reject("Referer checking failed - %s does not match %s." % + (referer, good_referer)) + + # If the user didn't already have a CSRF cookie, then fall back to + # the Django 1.1 method (hash of session ID), so a request is not + # rejected if the form was sent to the user before upgrading to the + # Django 1.2 method (session independent nonce) + if cookie_is_new: + try: + session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] + csrf_token = _make_legacy_session_token(session_id) + except KeyError: + # No CSRF cookie and no session cookie. For POST requests, + # we insist on a CSRF cookie, and in this way we can avoid + # all CSRF attacks, including login CSRF. + return reject("No CSRF or session cookie.") + else: + csrf_token = request.META["CSRF_COOKIE"] + + # check incoming token + request_csrf_token = request.POST.get('csrfmiddlewaretoken', None) + if request_csrf_token != csrf_token: + if cookie_is_new: + # probably a problem setting the CSRF cookie + return reject("CSRF cookie not set.") + else: + return reject("CSRF token missing or incorrect.") + + return accept() + + def process_response(self, request, response): + if getattr(response, 'csrf_processing_done', False): + return response + + # If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was + # never called, probaby because a request middleware returned a response + # (for example, contrib.auth redirecting to a login page). + if request.META.get("CSRF_COOKIE") is None: + return response + + if not request.META.get("CSRF_COOKIE_USED", False): + return response + + # Set the CSRF cookie even if it's already set, so we renew the expiry timer. + response.set_cookie(settings.CSRF_COOKIE_NAME, + request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52, + domain=settings.CSRF_COOKIE_DOMAIN) + # Content varies with the CSRF cookie, so set the Vary header. + patch_vary_headers(response, ('Cookie',)) + response.csrf_processing_done = True + return response + +class CsrfResponseMiddleware(object): + """ + DEPRECATED + Middleware that post-processes a response to add a csrfmiddlewaretoken. + + This exists for backwards compatibility and as an interim measure until + applications are converted to using use the csrf_token template tag + instead. It will be removed in Django 1.4. + """ + def __init__(self): + import warnings + warnings.warn( + "CsrfResponseMiddleware and CsrfMiddleware are deprecated; use CsrfViewMiddleware and the template tag instead (see CSRF documentation).", + PendingDeprecationWarning + ) + + def process_response(self, request, response): + if getattr(response, 'csrf_exempt', False): + return response + + if response['Content-Type'].split(';')[0] in _HTML_TYPES: + csrf_token = get_token(request) + # If csrf_token is None, we have no token for this request, which probably + # means that this is a response from a request middleware. + if csrf_token is None: + return response + + # ensure we don't add the 'id' attribute twice (HTML validity) + idattributes = itertools.chain(("id='csrfmiddlewaretoken'",), + itertools.repeat('')) + def add_csrf_field(match): + """Returns the matched tag plus the added element""" + return mark_safe(match.group() + "
" + \ + "
") + + # Modify any POST forms + response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content) + if n > 0: + # Content varies with the CSRF cookie, so set the Vary header. + patch_vary_headers(response, ('Cookie',)) + + # Since the content has been modified, any Etag will now be + # incorrect. We could recalculate, but only if we assume that + # the Etag was set by CommonMiddleware. The safest thing is just + # to delete. See bug #9163 + del response['ETag'] + return response + +class CsrfMiddleware(object): + """ + Django middleware that adds protection against Cross Site + Request Forgeries by adding hidden form fields to POST forms and + checking requests for the correct value. + + CsrfMiddleware uses two middleware, CsrfViewMiddleware and + CsrfResponseMiddleware, which can be used independently. It is recommended + to use only CsrfViewMiddleware and use the csrf_token template tag in + templates for inserting the token. + """ + # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware + # because both have process_response methods. + def __init__(self): + self.response_middleware = CsrfResponseMiddleware() + self.view_middleware = CsrfViewMiddleware() + + def process_response(self, request, resp): + # We must do the response post-processing first, because that calls + # get_token(), which triggers a flag saying that the CSRF cookie needs + # to be sent (done in CsrfViewMiddleware.process_response) + resp2 = self.response_middleware.process_response(request, resp) + return self.view_middleware.process_response(request, resp2) + + def process_view(self, request, callback, callback_args, callback_kwargs): + return self.view_middleware.process_view(request, callback, callback_args, + callback_kwargs) + diff --git a/django/template/__init__.py b/django/template/__init__.py index 5493e5bbb7..5b52d36089 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -942,8 +942,14 @@ class Library(object): else: t = get_template(file_name) self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict, - autoescape=context.autoescape)) + new_context = context_class(dict, autoescape=context.autoescape) + # Copy across the CSRF token, if present, because inclusion + # tags are often used for forms, and we need instructions + # for using CSRF protection to be as simple as possible. + csrf_token = context.get('csrf_token', None) + if csrf_token is not None: + new_context['csrf_token'] = csrf_token + return self.nodelist.render(new_context) compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode) compile_func.__doc__ = func.__doc__ diff --git a/django/template/context.py b/django/template/context.py index 1c43387468..f57a3aaa64 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -1,7 +1,12 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module +# Cache of actual callables. _standard_context_processors = None +# We need the CSRF processor no matter what the user has in their settings, +# because otherwise it is a security vulnerability, and we can't afford to leave +# this to human error or failure to read migration instructions. +_builtin_context_processors = ('django.core.context_processors.csrf',) class ContextPopException(Exception): "pop() has been called more times than push()" @@ -75,7 +80,10 @@ def get_standard_processors(): global _standard_context_processors if _standard_context_processors is None: processors = [] - for path in settings.TEMPLATE_CONTEXT_PROCESSORS: + collect = [] + collect.extend(_builtin_context_processors) + collect.extend(settings.TEMPLATE_CONTEXT_PROCESSORS) + for path in collect: i = path.rfind('.') module, attr = path[:i], path[i+1:] try: diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 47e116cd1a..2957c3d045 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -162,7 +162,7 @@ def floatformat(text, arg=-1): try: m = int(d) - d - except (OverflowError, InvalidOperation): + except (ValueError, OverflowError, InvalidOperation): return input_val if not m and p < 0: diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index de746997ab..6d57cdeef8 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -37,6 +37,23 @@ class CommentNode(Node): def render(self, context): return '' +class CsrfTokenNode(Node): + def render(self, context): + csrf_token = context.get('csrf_token', None) + if csrf_token: + if csrf_token == 'NOTPROVIDED': + return mark_safe(u"") + else: + return mark_safe(u"
" % (csrf_token)) + else: + # It's very probable that the token is missing because of + # misconfiguration, so we raise a warning + from django.conf import settings + if settings.DEBUG: + import warnings + warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") + return u'' + class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): self.cycle_iter = itertools_cycle(cyclevars) @@ -523,6 +540,10 @@ def cycle(parser, token): return node cycle = register.tag(cycle) +def csrf_token(parser, token): + return CsrfTokenNode() +register.tag(csrf_token) + def debug(parser, token): """ Outputs a whole load of debugging information, including the current diff --git a/django/test/client.py b/django/test/client.py index 348bfa7d26..63ad1c1d3a 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -66,6 +66,11 @@ class ClientHandler(BaseHandler): signals.request_started.send(sender=self.__class__) try: request = WSGIRequest(environ) + # sneaky little hack so that we can easily get round + # CsrfViewMiddleware. This makes life easier, and is probably + # required for backwards compatibility with external tests against + # admin views. + request._dont_enforce_csrf_checks = True response = self.get_response(request) # Apply response middleware. @@ -362,12 +367,18 @@ class Client(object): else: post_data = data + # Make `data` into a querystring only if it's not already a string. If + # it is a string, we'll assume that the caller has already encoded it. + query_string = None + if not isinstance(data, basestring): + query_string = urlencode(data, doseq=True) + parsed = urlparse(path) r = { 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, 'PATH_INFO': urllib.unquote(parsed[2]), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], + 'QUERY_STRING': query_string or parsed[4], 'REQUEST_METHOD': 'PUT', 'wsgi.input': FakePayload(post_data), } diff --git a/django/test/utils.py b/django/test/utils.py index cffb83dca4..6d80fe24f4 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,6 +1,7 @@ import sys, time, os from django.conf import settings from django.core import mail +from django.core.mail.backends import locmem from django.test import signals from django.template import Template from django.utils.translation import deactivate @@ -27,37 +28,22 @@ def instrumented_test_render(self, context): signals.template_rendered.send(sender=self, template=self, context=context) return self.nodelist.render(context) -class TestSMTPConnection(object): - """A substitute SMTP connection for use during test sessions. - The test connection stores email messages in a dummy outbox, - rather than sending them out on the wire. - - """ - def __init__(*args, **kwargs): - pass - def open(self): - "Mock the SMTPConnection open() interface" - pass - def close(self): - "Mock the SMTPConnection close() interface" - pass - def send_messages(self, messages): - "Redirect messages to the dummy outbox" - mail.outbox.extend(messages) - return len(messages) def setup_test_environment(): """Perform any global pre-test setup. This involves: - Installing the instrumented test renderer - - Diverting the email sending functions to a test buffer + - Set the email backend to the locmem email backend. - Setting the active locale to match the LANGUAGE_CODE setting. """ Template.original_render = Template.render Template.render = instrumented_test_render mail.original_SMTPConnection = mail.SMTPConnection - mail.SMTPConnection = TestSMTPConnection + mail.SMTPConnection = locmem.EmailBackend + + mail.original_email_backend = settings.EMAIL_BACKEND + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem' mail.outbox = [] @@ -76,8 +62,10 @@ def teardown_test_environment(): mail.SMTPConnection = mail.original_SMTPConnection del mail.original_SMTPConnection - del mail.outbox + settings.EMAIL_BACKEND = mail.original_email_backend + del mail.original_email_backend + del mail.outbox def get_runner(settings): test_path = settings.TEST_RUNNER.split('.') diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 4636a2d040..e3f16e462e 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -6,6 +6,19 @@ try: except ImportError: from django.utils.functional import wraps, update_wrapper # Python 2.3, 2.4 fallback. + +# Licence for MethodDecoratorAdaptor and auto_adapt_to_methods +# +# This code is taken from stackoverflow.com [1], the code being supplied by +# users 'Ants Aasma' [2] and 'Silent Ghost' [3] with modifications. It is +# legally included here under the terms of the Creative Commons +# Attribution-Share Alike 2.5 Generic Licence [4] +# +# [1] http://stackoverflow.com/questions/1288498/using-the-same-decorator-with-arguments-with-functions-and-methods +# [2] http://stackoverflow.com/users/107366/ants-aasma +# [3] http://stackoverflow.com/users/12855/silentghost +# [4] http://creativecommons.org/licenses/by-sa/2.5/ + class MethodDecoratorAdaptor(object): """ Generic way of creating decorators that adapt to being diff --git a/django/utils/functional.py b/django/utils/functional.py index 23614d1712..434b6b76c9 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -257,9 +257,8 @@ class LazyObject(object): A wrapper for another class that can be used to delay instantiation of the wrapped class. - This is useful, for example, if the wrapped class needs to use Django - settings at creation time: we want to permit it to be imported without - accessing settings. + By subclassing, you have the opportunity to intercept and alter the + instantiation. If you don't need to do that, use SimpleLazyObject. """ def __init__(self): self._wrapped = None @@ -267,9 +266,6 @@ class LazyObject(object): def __getattr__(self, name): if self._wrapped is None: self._setup() - if name == "__members__": - # Used to implement dir(obj) - return self._wrapped.get_all_members() return getattr(self._wrapped, name) def __setattr__(self, name, value): @@ -287,3 +283,68 @@ class LazyObject(object): """ raise NotImplementedError + # introspection support: + __members__ = property(lambda self: self.__dir__()) + + def __dir__(self): + if self._wrapped is None: + self._setup() + return dir(self._wrapped) + +class SimpleLazyObject(LazyObject): + """ + A lazy object initialised from any function. + + Designed for compound objects of unknown type. For builtins or objects of + known type, use django.utils.functional.lazy. + """ + def __init__(self, func): + """ + Pass in a callable that returns the object to be wrapped. + + If copies are made of the resulting SimpleLazyObject, which can happen + in various circumstances within Django, then you must ensure that the + callable can be safely run more than once and will return the same + value. + """ + self.__dict__['_setupfunc'] = func + # For some reason, we have to inline LazyObject.__init__ here to avoid + # recursion + self._wrapped = None + + def __str__(self): + if self._wrapped is None: self._setup() + return str(self._wrapped) + + def __unicode__(self): + if self._wrapped is None: self._setup() + return unicode(self._wrapped) + + def __deepcopy__(self, memo): + if self._wrapped is None: + # We have to use SimpleLazyObject, not self.__class__, because the + # latter is proxied. + result = SimpleLazyObject(self._setupfunc) + memo[id(self)] = result + return result + else: + import copy + return copy.deepcopy(self._wrapped, memo) + + # Need to pretend to be the wrapped class, for the sake of objects that care + # about this (especially in equality tests) + def __get_class(self): + if self._wrapped is None: self._setup() + return self._wrapped.__class__ + __class__ = property(__get_class) + + def __eq__(self, other): + if self._wrapped is None: self._setup() + return self._wrapped == other + + def __hash__(self): + if self._wrapped is None: self._setup() + return hash(self._wrapped) + + def _setup(self): + self._wrapped = self._setupfunc() diff --git a/django/views/csrf.py b/django/views/csrf.py new file mode 100644 index 0000000000..aa5e25b5b4 --- /dev/null +++ b/django/views/csrf.py @@ -0,0 +1,69 @@ +from django.http import HttpResponseForbidden +from django.template import Context, Template +from django.conf import settings + +# We include the template inline since we need to be able to reliably display +# this error message, especially for the sake of developers, and there isn't any +# other way of making it available independent of what is in the settings file. + +CSRF_FAILRE_TEMPLATE = """ + + + + + 403 Forbidden + + +

403 Forbidden

+

CSRF verification failed. Request aborted.

+ {% if DEBUG %} +

Help

+ {% if reason %} +

Reason given for failure:

+
+    {{ reason }}
+    
+ {% endif %} + +

In general, this can occur when there is a genuine Cross Site Request Forgery, or when + Django's + CSRF mechanism has not been used correctly. For POST forms, you need to + ensure:

+ +
    +
  • The view function uses RequestContext + for the template, instead of Context.
  • + +
  • In the template, there is a {% templatetag openblock %} csrf_token + {% templatetag closeblock %} template tag inside each POST form that + targets an internal URL.
  • + +
  • If you are not using CsrfViewMiddleware, then you must use + csrf_protect on any views that use the csrf_token + template tag, as well as those that accept the POST data.
  • + +
+ +

You're seeing the help section of this page because you have DEBUG = + True in your Django settings file. Change that to False, + and only the initial error message will be displayed.

+ +

You can customize this page using the CSRF_FAILURE_VIEW setting.

+ {% else %} +

More information is available with DEBUG=True.

+ + {% endif %} + + +""" + +def csrf_failure(request, reason=""): + """ + Default view used when request fails CSRF protection + """ + t = Template(CSRF_FAILRE_TEMPLATE) + c = Context({'DEBUG': settings.DEBUG, + 'reason': reason}) + return HttpResponseForbidden(t.render(c), mimetype='text/html') diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py new file mode 100644 index 0000000000..b789872efe --- /dev/null +++ b/django/views/decorators/csrf.py @@ -0,0 +1,47 @@ +from django.middleware.csrf import CsrfViewMiddleware +from django.utils.decorators import decorator_from_middleware +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + +csrf_protect = decorator_from_middleware(CsrfViewMiddleware) +csrf_protect.__name__ = "csrf_protect" +csrf_protect.__doc__ = """ +This decorator adds CSRF protection in exactly the same way as +CsrfViewMiddleware, but it can be used on a per view basis. Using both, or +using the decorator multiple times, is harmless and efficient. +""" + +def csrf_response_exempt(view_func): + """ + Modifies a view function so that its response is exempt + from the post-processing of the CSRF middleware. + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + resp.csrf_exempt = True + return resp + return wraps(view_func)(wrapped_view) + +def csrf_view_exempt(view_func): + """ + Marks a view function as being exempt from CSRF view protection. + """ + # We could just do view_func.csrf_exempt = True, but decorators + # are nicer if they don't have side-effects, so we return a new + # function. + def wrapped_view(*args, **kwargs): + return view_func(*args, **kwargs) + wrapped_view.csrf_exempt = True + return wraps(view_func)(wrapped_view) + +def csrf_exempt(view_func): + """ + Marks a view function as being exempt from the CSRF checks + and post processing. + + This is the same as using both the csrf_view_exempt and + csrf_response_exempt decorators. + """ + return csrf_response_exempt(csrf_view_exempt(view_func)) diff --git a/docs/faq/install.txt b/docs/faq/install.txt index fb8005c7e7..28a89ccc1f 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -18,7 +18,7 @@ How do I get started? What are Django's prerequisites? -------------------------------- -Django requires Python_, specifically any version of Python from 2.3 +Django requires Python_, specifically any version of Python from 2.4 through 2.6. No other Python libraries are required for basic Django usage. @@ -42,30 +42,35 @@ PostgreSQL fans, and MySQL_, `SQLite 3`_, and Oracle_ are also supported. .. _`SQLite 3`: http://www.sqlite.org/ .. _Oracle: http://www.oracle.com/ -Do I lose anything by using Python 2.3 versus newer Python versions, such as Python 2.5? ----------------------------------------------------------------------------------------- +Do I lose anything by using Python 2.4 versus newer Python versions, such as Python 2.5 or 2.6? +----------------------------------------------------------------------------------------------- -Not in the core framework. Currently, Django itself officially -supports any version of Python from 2.3 through 2.6, -inclusive. However, some add-on components may require a more recent -Python version; the ``django.contrib.gis`` component, for example, -requires at least Python 2.4, and third-party applications for use -with Django are, of course, free to set their own version -requirements. +Not in the core framework. Currently, Django itself officially supports any +version of Python from 2.4 through 2.6, inclusive. However, newer versions of +Python are often faster, have more features, and are better supported. +Third-party applications for use with Django are, of course, free to set their +own version requirements. -Please note, however, that over the next year or two Django will begin -dropping support for older Python versions as part of a migration -which will end with Django running on Python 3.0 (see next question -for details). So if you're just starting out with Python, it's -recommended that you use the latest 2.x release (currently, Python -2.6). This will let you take advantage of the numerous improvements -and optimizations to the Python language since version 2.3, and will -help ease the process of dropping support for older Python versions on -the road to Python 3.0. +Over the next year or two Django will begin dropping support for older Python +versions as part of a migration which will end with Django running on Python 3 +(see below for details). -Can I use Django with Python 3.0? +All else being equal, we recommend that you use the latest 2.x release +(currently Python 2.6). This will let you take advantage of the numerous +improvements and optimizations to the Python language since version 2.4, and +will help ease the process of dropping support for older Python versions on +the road to Python 3. + +Can I use Django with Python 2.3? --------------------------------- +Django 1.1 (and earlier) supported Python 2.3. Django 1.2 and newer does not. +We highly recommend you upgrade Python if at all possible, but Django 1.1 will +continue to work on Python 2.3. + +Can I use Django with Python 3? +------------------------------- + Not at the moment. Python 3.0 introduced a number of backwards-incompatible changes to the Python language, and although these changes are generally a good thing for Python's future, it will diff --git a/docs/index.txt b/docs/index.txt index 47abcafb1b..2c868f275f 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -202,7 +202,5 @@ The Django open-source project * **Django over time:** :ref:`API stability ` | - :ref:`Archive of release notes ` | `Backwards-incompatible changes`_ | + :ref:`Release notes ` | :ref:`Deprecation Timeline ` - -.. _Backwards-incompatible changes: http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 7326532ec9..d2eb80c710 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -148,7 +148,6 @@ Joseph Kocherhans .. _brian rosner: http://oebfare.com/ .. _eldarion: http://eldarion.com/ -.. _pinax: http://pinaxproject.com/ .. _django dose: http://djangodose.com/ `Gary Wilson`_ @@ -189,6 +188,31 @@ Karen Tracey Karen lives in Apex, NC, USA. +`Jannis Leidel`_ + Jannis graduated in media design from `Bauhaus-University Weimar`_, + is the author of a number of pluggable Django apps and likes to + contribute to Open Source projects like Pinax_. He currently works as + a freelance web developer and designer. + + Jannis lives in Berlin, Germany. + +.. _Jannis Leidel: http://jezdez.com/ +.. _Bauhaus-University Weimar: http://www.uni-weimar.de/ +.. _pinax: http://pinaxproject.com/ + +`James Tauber`_ + James is the lead developer of Pinax_ and the CEO and founder of + Eldarion_. He has been doing open source software since 1993, Python + since 1998 and Django since 2006. He serves on the board of the Python + Software Foundation and is currently on a leave of absence from a PhD in + linguistics. + + James currently lives in Boston, MA, USA but originally hails from + Perth, Western Australia where he attended the same high school as + Russell Keith-Magee. + +.. _James Tauber: http://jtauber.com/ + Specialists ----------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 7e7f4c6338..480b527d6b 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -13,6 +13,21 @@ their deprecation, as per the :ref:`Django deprecation policy hooking up admin URLs. This has been deprecated since the 1.1 release. + * 1.4 + * ``CsrfResponseMiddleware``. This has been deprecated since the 1.2 + release, in favour of the template tag method for inserting the CSRF + token. ``CsrfMiddleware``, which combines ``CsrfResponseMiddleware`` + and ``CsrfViewMiddleware``, is also deprecated. + + * The old imports for CSRF functionality (``django.contrib.csrf.*``), + which moved to core in 1.2, will be removed. + + * ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection`` + class in favor of a generic E-mail backend API. + + * The many to many SQL generation functions on the database backends + will be removed. These have been deprecated since the 1.2 release. + * 2.0 * ``django.views.defaults.shortcut()``. This function has been moved to ``django.contrib.contenttypes.views.shortcut()`` as part of the diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt index 6d4ad9e8c9..e990ab8ab6 100644 --- a/docs/internals/release-process.txt +++ b/docs/internals/release-process.txt @@ -56,7 +56,7 @@ These releases will contain new features, improvements to existing features, and such. A minor release may deprecate certain features from previous releases. If a feature in version ``A.B`` is deprecated, it will continue to work in version ``A.B+1``. In version ``A.B+2``, use of the feature will raise a -``PendingDeprecationWarning`` but will continue to work. Version ``A.B+3`` will +``DeprecationWarning`` but will continue to work. Version ``A.B+3`` will remove the feature entirely. So, for example, if we decided to remove a function that existed in Django 1.0: diff --git a/docs/intro/install.txt b/docs/intro/install.txt index 237c208f2a..d0776a6ea3 100644 --- a/docs/intro/install.txt +++ b/docs/intro/install.txt @@ -12,7 +12,7 @@ Install Python -------------- Being a Python Web framework, Django requires Python. It works with any Python -version from 2.3 to 2.6 (due to backwards +version from 2.4 to 2.6 (due to backwards incompatibilities in Python 3.0, Django does not currently work with Python 3.0; see :ref:`the Django FAQ ` for more information on supported Python versions and the 3.0 transition), but we recommend installing Python 2.5 or later. If you do so, you won't need to set up a database just yet: Python 2.5 or later includes a lightweight database called SQLite_. diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 99aebff2c5..ea4dff61bd 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -282,6 +282,7 @@ That'll create a directory :file:`polls`, which is laid out like this:: polls/ __init__.py models.py + tests.py views.py This directory structure will house the poll application. diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 203c945c02..ad1bd9d990 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -34,11 +34,11 @@ activate the admin site for your installation, do these three things: * Run ``python manage.py syncdb``. Since you have added a new application to :setting:`INSTALLED_APPS`, the database tables need to be updated. - * Edit your ``mysite/urls.py`` file and uncomment the lines below the - "Uncomment the next two lines..." comment. This file is a URLconf; - we'll dig into URLconfs in the next tutorial. For now, all you need to - know is that it maps URL roots to applications. In the end, you should - have a ``urls.py`` file that looks like this: + * Edit your ``mysite/urls.py`` file and uncomment the lines that reference + the admin -- there are three lines in total to uncomment. This file is a + URLconf; we'll dig into URLconfs in the next tutorial. For now, all you + need to know is that it maps URL roots to applications. In the end, you + should have a ``urls.py`` file that looks like this: .. versionchanged:: 1.1 The method for adding admin urls has changed in Django 1.1. diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 238dc63f71..1438a9e776 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -171,15 +171,23 @@ and put the following Python code in it:: This is the simplest view possible. Go to "/polls/" in your browser, and you should see your text. -Now add the following view. It's slightly different, because it takes an -argument (which, remember, is passed in from whatever was captured by the -regular expression in the URLconf):: +Now lets add a few more views. These views are slightly different, because +they take an argument (which, remember, is passed in from whatever was +captured by the regular expression in the URLconf):: def detail(request, poll_id): return HttpResponse("You're looking at poll %s." % poll_id) -Take a look in your browser, at "/polls/34/". It'll display whatever ID you -provide in the URL. + def results(request, poll_id): + return HttpResponse("You're looking at the results of poll %s." % poll_id) + + def vote(request, poll_id): + return HttpResponse("You're voting on poll %s." % poll_id) + +Take a look in your browser, at "/polls/34/". It'll run the `detail()` method +and display whatever ID you provide in the URL. Try "/polls/34/results/" and +"/polls/34/vote/" too -- these will display the placeholder results and voting +pages. Write views that actually do something ====================================== @@ -467,10 +475,10 @@ Copy the file ``mysite/urls.py`` to ``mysite/polls/urls.py``. Then, change ``mysite/urls.py`` to remove the poll-specific URLs and insert an :func:`~django.conf.urls.defaults.include`:: - ... + # ... urlpatterns = patterns('', (r'^polls/', include('mysite.polls.urls')), - ... + # ... :func:`~django.conf.urls.defaults.include`, simply, references another URLconf. Note that the regular expression doesn't have a ``$`` (end-of-string match diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 28ace85ca8..394fc25ea8 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -21,6 +21,7 @@ tutorial, so that the template contains an HTML ```` element: {% if error_message %}

{{ error_message }}

{% endif %} + {% csrf_token %} {% for choice in poll.choice_set.all %}
@@ -46,17 +47,41 @@ A quick rundown: * ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone through its loop + * Since we are creating a POST form (which can have the effect of modifying + data), we unfortunately need to worry about Cross Site Request Forgeries. + Thankfully, you don't have to worry too hard, because Django comes with + very easy-to-use system for protecting against it. In short, all POST + forms that are targetted at internal URLs need the ``{% csrf_token %}`` + template tag adding. + +The ``{% csrf_token %}`` tag requires information from the request object, which +is not normally accessible from within the template context. To fix this, a +small adjustment needs to be made to the ``detail`` view, so that it looks like +the following:: + + from django.template import RequestContext + # ... + def detail(request, poll_id): + p = get_object_or_404(Poll, pk=poll_id) + return render_to_response('polls/detail.html', {'poll': p}, + context_instance=RequestContext(request)) + +The details of how this works are explained in the documentation for +:ref:`RequestContext `. + Now, let's create a Django view that handles the submitted data and does something with it. Remember, in :ref:`Tutorial 3 `, we created a URLconf for the polls application that includes this line:: (r'^(?P\d+)/vote/$', 'vote'), -So let's create a ``vote()`` function in ``mysite/polls/views.py``:: +We also created a dummy implementation of the ``vote()`` function. Let's +create a real version. Add the following to ``mysite/polls/views.py``:: from django.shortcuts import get_object_or_404, render_to_response - from django.http import HttpResponseRedirect + from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse + from django.template import RequestContext from mysite.polls.models import Choice, Poll # ... def vote(request, poll_id): @@ -68,7 +93,7 @@ So let's create a ``vote()`` function in ``mysite/polls/views.py``:: return render_to_response('polls/detail.html', { 'poll': p, 'error_message': "You didn't select a choice.", - }) + }, context_instance=RequestContext(request)) else: selected_choice.votes += 1 selected_choice.save() diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index c1e05eda1d..0f746bf01b 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -770,7 +770,7 @@ documented in :ref:`topics-http-urls`:: However, the ``self.my_view`` function registered above suffers from two problems: - * It will *not* perform and permission checks, so it will be accessible to + * It will *not* perform any permission checks, so it will be accessible to the general public. * It will *not* provide any header details to prevent caching. This means if the page retrieves data from the database, and caching middleware is @@ -1048,16 +1048,70 @@ automatically:: FriendshipInline, ] +Working with Many-to-Many Models +-------------------------------- + +.. versionadded:: 1.2 + +By default, admin widgets for many-to-many relations will be displayed +on whichever model contains the actual reference to the ``ManyToManyField``. +Depending on your ``ModelAdmin`` definition, each many-to-many field in your +model will be represented by a standard HTML ``