diff --git a/AUTHORS b/AUTHORS index 294ad187c9..56d5e49cb8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -104,6 +104,7 @@ answer newbie questions, and generally made Django that much better: Jure Cuhalev John D'Agostino dackze+django@gmail.com + Mihai Damian David Danier Dirk Datzert Jonathan Daugherty (cygnus) @@ -347,7 +348,7 @@ answer newbie questions, and generally made Django that much better: Zach Thompson Michael Thornhill Deepak Thukral - tibimicu@gmax.net + tibimicu@gmx.net tobias@neuyork.de Tom Tobin Joe Topjian diff --git a/django/bin/make-messages.py b/django/bin/make-messages.py index 44040390ea..02369c88fd 100755 --- a/django/bin/make-messages.py +++ b/django/bin/make-messages.py @@ -85,8 +85,7 @@ def make_messages(): src = pythonize_re.sub('\n#', src) open(os.path.join(dirpath, '%s.py' % file), "wb").write(src) thefile = '%s.py' % file - cmd = 'xgettext %s -d %s -L Perl --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --from-code UTF-8 -o - "%s"' % ( - os.path.exists(potfile) and '--omit-header' or '', domain, os.path.join(dirpath, thefile)) + cmd = 'xgettext -d %s -L Perl --keyword=gettext_noop --keyword=gettext_lazy --keyword=ngettext_lazy:1,2 --from-code UTF-8 -o - "%s"' % (domain, os.path.join(dirpath, thefile)) (stdin, stdout, stderr) = os.popen3(cmd, 't') msgs = stdout.read() errors = stderr.read() @@ -97,6 +96,11 @@ def make_messages(): old = '#: '+os.path.join(dirpath, thefile)[2:] new = '#: '+os.path.join(dirpath, file)[2:] msgs = msgs.replace(old, new) + if os.path.exists(potfile): + # Strip the header + msgs = '\n'.join(dropwhile(len, msgs.split('\n'))) + else: + msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8') if msgs: open(potfile, 'ab').write(msgs) os.unlink(os.path.join(dirpath, thefile)) diff --git a/django/conf/locale/ca/LC_MESSAGES/django.mo b/django/conf/locale/ca/LC_MESSAGES/django.mo index 9c902e8e38..ee65dfe534 100644 Binary files a/django/conf/locale/ca/LC_MESSAGES/django.mo and b/django/conf/locale/ca/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ca/LC_MESSAGES/django.po b/django/conf/locale/ca/LC_MESSAGES/django.po index ca1049ccf2..6c99137f21 100644 --- a/django/conf/locale/ca/LC_MESSAGES/django.po +++ b/django/conf/locale/ca/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2007-02-15 01:00+0200\n" +"POT-Creation-Date: 2008-04-25 00:18+0200\n" "PO-Revision-Date: 2008-03-29 01:37+0100\n" "Last-Translator: Django Catalan Group \n" "Language-Team: Catalan \n" @@ -63,7 +63,6 @@ msgid "Argentinean Spanish" msgstr "Castellà Argentí" #: conf/global_settings.py:51 -#, fuzzy msgid "Basque" msgstr "Euskera" @@ -152,8 +151,8 @@ msgid "Portugese" msgstr "Portuguès" #: conf/global_settings.py:73 -msgid "Brazilian" -msgstr "Brasileny" +msgid "Brazilian Portuguese" +msgstr "Portuguès de Brasil" #: conf/global_settings.py:74 msgid "Romanian" @@ -401,8 +400,8 @@ msgid "" "following types of objects:" msgstr "" "Eliminar el/la %(object_name)s '%(escaped_object)s' provocaria l'eliminació " -"d'objectes relacionats, però el vostre compte no te permissos per a esborrar els " -"tipus d'objecte següents:" +"d'objectes relacionats, però el vostre compte no te permissos per a esborrar " +"els tipus d'objecte següents:" #: contrib/admin/templates/admin/delete_confirmation.html:20 #, python-format @@ -410,7 +409,7 @@ msgid "" "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " "All of the following related items will be deleted:" msgstr "" -"Està segur de voler esborrar els/les %(object_name)s \"%(escaped_object)s\"? " +"Esteu segurs de voler esborrar els/les %(object_name)s \"%(escaped_object)s\"? " "S'esborraran els següents elements relacionats:" #: contrib/admin/templates/admin/delete_confirmation.html:25 @@ -429,7 +428,7 @@ msgstr "Filtre" #: contrib/admin/templates/admin/index.html:17 #, python-format msgid "Models available in the %(name)s application." -msgstr "Models disponibles en l'aplicació %(name)s." +msgstr "Models disponibles a l'aplicació %(name)s." #: contrib/admin/templates/admin/index.html:18 #, python-format @@ -547,7 +546,7 @@ msgid "" "First, enter a username and password. Then, you'll be able to edit more user " "options." msgstr "" -"Primer, entri un usuari i una contrasenya. Després podrà editar més opcions " +"Primer, entreu un usuari i una contrasenya. Després podreu editar més opcions " "de l'usuari." #: contrib/admin/templates/admin/auth/user/add_form.html:12 @@ -593,15 +592,15 @@ msgid "" "your computer is \"internal\").

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

Per a instal·lar 'bookmarklets', arrosegui l'enllaç a la " +"

Per a instal·lar 'bookmarklets', arrosegueu l'enllaç a la " "seva barra de\n" -"marcadors, o faci click amb el botò dret en l'enllaç i afegeixi'l als " +"marcadors, o feu click amb el botó dret en l'enllaç i afegeixi'l als " "marcadors.\n" -"Ara pot escollir el 'bookmarklet' des de qualsevol pàgina del lloc.\n" -"Observi que alguns d'aquests 'bookmarklets' precisen que estigui veient\n" -"el lloc des de un ordinador senyalat com a \"intern\" (parli\n" -"amb el seu administrador de sistemes si no està segur de la condició del " -"seu).

\n" +"Ara podeu escollir el 'bookmarklet' des de qualsevol pàgina del lloc.\n" +"Observeu que alguns d'aquests 'bookmarklets' precisen que estigui veient\n" +"el lloc des de un ordinador senyalat com a \"intern\" (parleu\n" +"amb el vostre administrador de sistemes si no esteu segurs de la condició del " +"vostre).

\n" #: contrib/admin/templates/admin_doc/bookmarklets.html:18 msgid "Documentation for this page" @@ -612,7 +611,7 @@ msgid "" "Jumps you from any page to the documentation for the view that generates " "that page." msgstr "" -"El porta des de qualsevol pàgina de la documentació a la vista que la genera." +"Us porta des de qualsevol pàgina de la documentació a la vista que la genera." #: contrib/admin/templates/admin_doc/bookmarklets.html:21 msgid "Show object ID" @@ -673,9 +672,9 @@ msgid "" "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." msgstr "" -"Si us plau, introdueixi la seva contrasenya antiga, per seguretat, i tot " -"seguit introdueixi la seva contrasenya nova dues vegades per verificar que " -"l'ha escrit correctament." +"Si us plau, introduïu la vostra contrasenya antiga, per seguretat, i tot " +"seguit introduïu la vostra contrasenya nova dues vegades per verificar que " +"l'heu escrit correctament." #: contrib/admin/templates/registration/password_change_form.html:16 msgid "Old password:" @@ -703,39 +702,39 @@ msgstr "Restablir contrasenya" #: contrib/admin/templates/registration/password_reset_done.html:6 #: contrib/admin/templates/registration/password_reset_done.html:10 msgid "Password reset successful" -msgstr "Restabliment de contrasenya exitòs" +msgstr "Restabliment de contrasenya exitós" #: contrib/admin/templates/registration/password_reset_done.html:12 msgid "" "We've e-mailed a new password to the e-mail address you submitted. You " "should be receiving it shortly." msgstr "" -"Li hem enviat una contrasenya nova a l'adreça de correu electrònic que ens " -"ha indicat. L'hauria de rebre en breu." +"Us hem enviat una contrasenya nova a l'adreça de correu electrònic que ens " +"heu indicat. L'haurieu de rebre en breu." #: contrib/admin/templates/registration/password_reset_email.html:2 msgid "You're receiving this e-mail because you requested a password reset" msgstr "" -"Està rebent aquest missatge degut a que va solicitar un restabliment de " +"Esteu rebent aquest missatge degut a que veu solicitar un restabliment de " "contrasenya." #: contrib/admin/templates/registration/password_reset_email.html:3 #, python-format msgid "for your user account at %(site_name)s" -msgstr "del seu compte d'usuari a %(site_name)s." +msgstr "del vostre compte d'usuari a %(site_name)s." #: contrib/admin/templates/registration/password_reset_email.html:5 #, python-format msgid "Your new password is: %(new_password)s" -msgstr "La seva nova contrasenya és: %(new_password)s" +msgstr "La vostra nova contrasenya és: %(new_password)s" #: contrib/admin/templates/registration/password_reset_email.html:7 msgid "Feel free to change this password by going to this page:" -msgstr "Sentis lliure de canviar-la en aquesta pàgina:" +msgstr "Sentiu-vos lliures de canviar-la en aquesta pàgina:" #: contrib/admin/templates/registration/password_reset_email.html:11 msgid "Your username, in case you've forgotten:" -msgstr "El seu nom d'usuari, en cas d'haver-lo oblidat:" +msgstr "El vostre nom d'usuari, en cas d'haver-lo oblidat:" #: contrib/admin/templates/registration/password_reset_email.html:13 msgid "Thanks for using our site!" @@ -751,8 +750,8 @@ msgid "" "Forgotten your password? Enter your e-mail address below, and we'll reset " "your password and e-mail the new one to you." msgstr "" -"Ha oblidat la seva contrasenya? Introdueixi la seva adreça de correu " -"electrònic i en crearem una de nova que li enviarem per correu." +"Heu oblidat la vostra contrasenya? Introduïu la vostra adreça de correu " +"electrònic i en crearem una de nova que us enviarem per correu." #: contrib/admin/templates/registration/password_reset_form.html:16 msgid "E-mail address:" @@ -785,12 +784,12 @@ msgstr "Totes les dates" #: contrib/admin/views/auth.py:20 contrib/admin/views/main.py:267 #, python-format msgid "The %(name)s \"%(obj)s\" was added successfully." -msgstr "El/la %(name)s \"%(obj)s\".ha estat agregat/da amb èxit." +msgstr "El/la %(name)s \"%(obj)s\".ha estat afegit/da amb èxit." #: contrib/admin/views/auth.py:25 contrib/admin/views/main.py:271 #: contrib/admin/views/main.py:356 msgid "You may edit it again below." -msgstr "Pot editar-lo de nou a baix." +msgstr "Podeu editar-lo de nou a baix." #: contrib/admin/views/auth.py:31 msgid "Add user" @@ -810,7 +809,7 @@ msgid "" "Please enter a correct username and password. Note that both fields are case-" "sensitive." msgstr "" -"Si us plau, introdueixi un nom d'usuari i contrasenya vàlids. Tingui en " +"Si us plau, introduïu un nom d'usuari i contrasenya vàlids. Tingueu en " "compte que tots dos camps son sensibles a majúscules i minúscules." #: contrib/admin/views/decorators.py:69 @@ -818,17 +817,17 @@ msgid "" "Please log in again, because your session has expired. Don't worry: Your " "submission has been saved." msgstr "" -"Si us plau, identifiquis de nou doncs la seva sessió ha expirat. No es " -"preocupi, el seu enviament està emmagatzemat." +"Si us plau, identifiqueu-vos de nou doncs la vostra sessió ha expirat. No us " +"preocupeu, el seu enviament està emmagatzemat." #: contrib/admin/views/decorators.py:76 msgid "" "Looks like your browser isn't configured to accept cookies. Please enable " "cookies, reload this page, and try again." msgstr "" -"Sembla ser que el seu navegador no està configurat per acceptar " -"'cookies' (galetes). Si us plau, habiliti les 'cookies', recarregui aquesta " -"pàgina i provi-ho de nou. " +"Sembla ser que el vostre navegador no està configurat per acceptar " +"'cookies' (galetes). Si us plau, habiliteu les 'cookies', recarregueu aquesta " +"pàgina i proveu-ho de nou. " #: contrib/admin/views/decorators.py:90 msgid "Usernames cannot contain the '@' character." @@ -838,7 +837,7 @@ msgstr "Els noms d'usuari no poden contenir el caracter '@'." #, python-format msgid "Your e-mail address is not your username. Try '%s' instead." msgstr "" -"La seva adreça de correu no és el seu nom d'usuari. Provi '%s' en tot cas." +"La vostra adreça de correu no és el vostre nom d'usuari. Provi '%s' en tot cas." #: contrib/admin/views/doc.py:48 contrib/admin/views/doc.py:50 #: contrib/admin/views/doc.py:52 @@ -963,13 +962,13 @@ msgstr "Text" msgid "Time" msgstr "Hora" -#: contrib/admin/views/doc.py:318 contrib/flatpages/models.py:7 +#: contrib/admin/views/doc.py:318 contrib/flatpages/models.py:8 msgid "URL" msgstr "URL" #: contrib/admin/views/doc.py:319 msgid "U.S. state (two uppercase letters)" -msgstr "Estat dels E.U.A. (dos lletres majúscules)" +msgstr "Estat dels E.U.A. (dues lletres majúscules)" #: contrib/admin/views/doc.py:320 msgid "XML text" @@ -987,7 +986,7 @@ msgstr "Lloc administratiu" #: contrib/admin/views/main.py:280 contrib/admin/views/main.py:365 #, python-format msgid "You may add another %s below." -msgstr "Pot afegir un altre %s a baix." +msgstr "Podeu afegir un altre %s a baix." #: contrib/admin/views/main.py:298 #, python-format @@ -1029,7 +1028,8 @@ msgstr "S'ha modificat amb èxit el/la %(name)s \"%(obj)s." msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." msgstr "" -"S'ha agregat exitosament el/la %(name)s \"%(obj)s\". Pot editar-lo de nou abaix." +"S'ha afegit exitosament el/la %(name)s \"%(obj)s\". Pot editar-lo de nou " +"abaix." #: contrib/admin/views/main.py:400 #, python-format @@ -1063,12 +1063,12 @@ msgstr "Modificar històric: %s" #: contrib/admin/views/main.py:583 #, python-format msgid "Select %s" -msgstr "Seleccioni %s" +msgstr "Seleccioneu %s" #: contrib/admin/views/main.py:583 #, python-format msgid "Select %s to change" -msgstr "Seleccioni %s per modificar" +msgstr "Seleccioneu %s per modificar" #: contrib/admin/views/main.py:784 msgid "Database error" @@ -1099,7 +1099,7 @@ msgid "" "That e-mail address doesn't have an associated user account. Are you sure " "you've registered?" msgstr "" -"Aquesta adreça de correu no té associada cap compte d'usuari. Està segur de " +"Aquesta adreça de correu no té associada cap compte d'usuari. Esteu segurs de " "que s'ha registrat?" #: contrib/auth/forms.py:107 @@ -1114,7 +1114,7 @@ msgstr "Els dos camps de nova contrasenya no coincideixen." #: contrib/auth/forms.py:124 msgid "Your old password was entered incorrectly. Please enter it again." msgstr "" -"La seva antiga contrasenya no és correcte. Si el plau, introdueixi-la de nou." +"La seva antiga contrasenya no és correcte. Si el plau, introduïu-la de nou." #: contrib/auth/models.py:73 contrib/auth/models.py:93 msgid "name" @@ -1173,7 +1173,7 @@ msgid "" "Use '[algo]$[salt]$[hexdigest]' or use the change " "password form." msgstr "" -"Utilitzi '[algo]$[salt]$[hexdigest]' o el formulari de " +"Utilitzeu '[algo]$[salt]$[hexdigest]' o el formulari de " "canvi de contrasenya." #: contrib/auth/models.py:136 @@ -1189,12 +1189,13 @@ msgid "active" msgstr "actiu" #: contrib/auth/models.py:137 +#, fuzzy msgid "" -"Designates whether this user can log into the Django admin. Unselect this " +"Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -"Designa si aquest usuari pot iniciar sessió a la interfície administrativa " -"Django. Deseleccioni-ho enlloc d'esborrar comptes d'usuari." +"Designa si aquest usuari ha de ser tractat com a actiu. Desekeccioneu-ho " +"enlloc d'esborrar comptes d'usuari." #: contrib/auth/models.py:138 msgid "superuser status" @@ -1242,7 +1243,7 @@ msgstr "Informació personal" #: contrib/auth/models.py:155 msgid "Permissions" -msgstr "permissos" +msgstr "permisos" #: contrib/auth/models.py:156 msgid "Important dates" @@ -1326,7 +1327,7 @@ msgid "" "Check this box if the comment is inappropriate. A \"This comment has been " "removed\" message will be displayed instead." msgstr "" -"Marqui aquesta caixa si el comentari no és apropiat. En lloc seu es mostrarà " +"Marqueu aquesta caixa si el comentari no és apropiat. En lloc seu es mostrarà " "\"Aquest comentari ha estat eliminat\" " #: contrib/comments/models.py:96 @@ -1477,7 +1478,7 @@ msgstr "El seu nom:" #: contrib/comments/views/comments.py:28 msgid "" "This rating is required because you've entered at least one other rating." -msgstr "Es precisa aquesta puntuació perquè has introduït almenys un altre." +msgstr "Es precisa aquesta puntuació perquè n'heu introduït almenys una altra." #: contrib/comments/views/comments.py:112 #, python-format @@ -1492,13 +1493,13 @@ msgid_plural "" "\n" "%(text)s" msgstr[0] "" -"Aquest comentari va ser enviat per un usuari que ha enviat menys de %" -"(count)s comentari:\n" +"Aquest comentari va ser enviat per un usuari que ha enviat menys de %(count)" +"s comentari:\n" "\n" "%(text)s" msgstr[1] "" -"Aquest comentari va ser enviat per un usuari que ha enviat menys de %" -"(count)s comentaris:\n" +"Aquest comentari va ser enviat per un usuari que ha enviat menys de %(count)" +"s comentaris:\n" "\n" "%(text)s" @@ -1553,7 +1554,7 @@ msgstr "ID del comentari invàlid" #: contrib/comments/views/karma.py:27 msgid "No voting for yourself" -msgstr "No pots votar-te a tu mateix" +msgstr "No podeu votar-vos a vosaltres mateixos" #: contrib/contenttypes/models.py:67 msgid "python model class name" @@ -1567,30 +1568,30 @@ msgstr "tipus de contingut" msgid "content types" msgstr "tipus de continguts" -#: contrib/flatpages/models.py:8 +#: contrib/flatpages/models.py:9 msgid "" "Example: '/about/contact/'. Make sure to have leading and trailing slashes." msgstr "" -"Exemple: '/about/contact/'. Asseguri's de posar les barres al principi i al " +"Exemple: '/about/contact/'. Assegureu-vos de posar les barres al principi i al " "final." -#: contrib/flatpages/models.py:9 +#: contrib/flatpages/models.py:10 msgid "title" msgstr "títol" -#: contrib/flatpages/models.py:10 +#: contrib/flatpages/models.py:11 msgid "content" msgstr "contingut" -#: contrib/flatpages/models.py:11 +#: contrib/flatpages/models.py:12 msgid "enable comments" msgstr "habilitar comentaris" -#: contrib/flatpages/models.py:12 +#: contrib/flatpages/models.py:13 msgid "template name" msgstr "nom de la plantilla" -#: contrib/flatpages/models.py:13 +#: contrib/flatpages/models.py:14 msgid "" "Example: 'flatpages/contact_page.html'. If this isn't provided, the system " "will use 'flatpages/default.html'." @@ -1598,22 +1599,26 @@ msgstr "" "Exemple: 'flatpages/contact_page.html'. Si no es proporciona, el sistema " "utilitzarà 'flatpages/default.htmlt'." -#: contrib/flatpages/models.py:14 +#: contrib/flatpages/models.py:15 msgid "registration required" msgstr "cal estar registrat" -#: contrib/flatpages/models.py:14 +#: contrib/flatpages/models.py:15 msgid "If this is checked, only logged-in users will be able to view the page." msgstr "Si està marcat, només els usuaris registrats podran veure la pàgina." -#: contrib/flatpages/models.py:18 +#: contrib/flatpages/models.py:20 msgid "flat page" msgstr "pàgina estàtica" -#: contrib/flatpages/models.py:19 +#: contrib/flatpages/models.py:21 msgid "flat pages" msgstr "pàgines estàtiques" +#: contrib/flatpages/models.py:27 +msgid "Advanced options" +msgstr "" + #: contrib/humanize/templatetags/humanize.py:19 msgid "th" msgstr "rt" @@ -1701,7 +1706,7 @@ msgstr "ahir" #: contrib/localflavor/ar/forms.py:27 msgid "Enter a postal code in the format NNNN or ANNNNAAA." -msgstr "Introdueixi un codi postal en el format NNNN o ANNNNAAA." +msgstr "Introduïu un codi postal en el format NNNN o ANNNNAAA." #: contrib/localflavor/ar/forms.py:49 contrib/localflavor/br/forms.py:96 #: contrib/localflavor/br/forms.py:135 contrib/localflavor/pe/forms.py:23 @@ -1716,7 +1721,7 @@ msgstr "Aquest camp precisa 7 o 8 dígits." #: contrib/localflavor/ar/forms.py:79 msgid "Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format." msgstr "" -"Introdueixi un número CUIT vàlid en el format XX-XXXXXXXX-X o XXXXXXXXXXXX." +"Introduïu un número CUIT vàlid en el format XX-XXXXXXXX-X o XXXXXXXXXXXX." #: contrib/localflavor/ar/forms.py:80 msgid "Invalid CUIT." @@ -1724,11 +1729,11 @@ msgstr "Invàlid CUIT." #: contrib/localflavor/au/forms.py:16 msgid "Enter a 4 digit post code." -msgstr "Introdueixi un codi postal de 4 dígits." +msgstr "Introduïu un codi postal de 4 dígits." #: contrib/localflavor/br/forms.py:21 msgid "Enter a zip code in the format XXXXX-XXX." -msgstr "Introdueixi un codi zip en el format XXXXX-XXX." +msgstr "Introduïu un codi zip en el format XXXXX-XXX." #: contrib/localflavor/br/forms.py:30 msgid "Phone numbers must be in XX-XXXX-XXXX format." @@ -1739,7 +1744,7 @@ msgid "" "Select a valid brazilian state. That state is not one of the available " "states." msgstr "" -"Seleccioni un estat brasiler vàlid. Aquest estat no és un dels estats " +"Seleccioneu un estat brasiler vàlid. Aquest estat no és un dels estats " "disponibles." #: contrib/localflavor/br/forms.py:94 @@ -1760,12 +1765,12 @@ msgstr "Aquest camp precisa almenys 14 dígits." #: contrib/localflavor/ca/forms.py:17 msgid "Enter a postal code in the format XXX XXX." -msgstr "Introdueixi un codi postal en el format XXX XXX." +msgstr "Introduïu un codi postal en el format XXX XXX." #: contrib/localflavor/ca/forms.py:88 msgid "Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format." msgstr "" -"Introdueixi un número vàlid de la Assegurança Social de Canadà en el format " +"Introduïu un número vàlid de la Assegurança Social de Canadà en el format " "XXX-XXX-XXX." #: contrib/localflavor/ch/ch_states.py:5 @@ -1874,23 +1879,23 @@ msgstr "Zuric" #: contrib/localflavor/ch/forms.py:16 contrib/localflavor/no/forms.py:12 msgid "Enter a zip code in the format XXXX." -msgstr "Introdueixi un codi zip en el format XXXX." +msgstr "Introduïu un codi zip en el format XXXX." #: contrib/localflavor/ch/forms.py:64 msgid "" "Enter a valid Swiss identity or passport card number in X1234567<0 or " "1234567890 format." msgstr "" -"Introdueixi un número d'identificació o de passaport Suïssos en els formats " +"Introduïu un número d'identificació o de passaport Suïssos en els formats " "1234567890 o X1234567<0." #: contrib/localflavor/cl/forms.py:29 msgid "Enter a valid Chilean RUT." -msgstr "Introdueixi un RUT Xilè vàlid." +msgstr "Introduïu un RUT Xilè vàlid." #: contrib/localflavor/cl/forms.py:30 msgid "Enter a valid Chilean RUT. The format is XX.XXX.XXX-X." -msgstr "Introdueixi un RUT Xilè vàlid. El format és XX.XXX.XXX-X" +msgstr "Introduïu un RUT Xilè vàlid. El format és XX.XXX.XXX-X" #: contrib/localflavor/cl/forms.py:31 msgid "The Chilean RUT is not valid." @@ -1963,14 +1968,14 @@ msgstr "Turíngia" #: contrib/localflavor/de/forms.py:14 contrib/localflavor/fi/forms.py:12 #: contrib/localflavor/fr/forms.py:15 msgid "Enter a zip code in the format XXXXX." -msgstr "Introdueixi un codi zip en el format XXXXX." +msgstr "Introduïu un codi zip en el format XXXXX." #: contrib/localflavor/de/forms.py:41 msgid "" "Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X " "format." msgstr "" -"Introdueixi un número de tarjeta d'identificació alemany vàlid en el format " +"Introduïu un número de tarjeta d'identificació alemany vàlid en el format " "XXXXXXXXXXX-XXXXXXX-XXXXXXX-X." #: contrib/localflavor/es/es_provinces.py:5 @@ -2242,23 +2247,23 @@ msgstr "Comunitat Valenciana" #: contrib/localflavor/es/forms.py:19 msgid "Enter a valid postal code in the range and format 01XXX - 52XXX." -msgstr "Introdueixi un codi postal en rang i format 01XXX - 52XXX." +msgstr "Introduïu un codi postal en rang i format 01XXX - 52XXX." #: contrib/localflavor/es/forms.py:39 msgid "" "Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or " "9XXXXXXXX." msgstr "" -"Introdueixi un número de telèfon vàlid en un dels formats 6XXXXXXXX, " +"Introduïu un número de telèfon vàlid en un dels formats 6XXXXXXXX, " "8XXXXXXXX o 9XXXXXXXX." #: contrib/localflavor/es/forms.py:66 msgid "Please enter a valid NIF, NIE, or CIF." -msgstr "Si us plau, introdueixi un NIF, NIE o CIF vàlid." +msgstr "Si us plau, introduïu un NIF, NIE o CIF vàlid." #: contrib/localflavor/es/forms.py:67 msgid "Please enter a valid NIF or NIE." -msgstr "Si us plau, introdueixi un NIF o NIE vàlid." +msgstr "Si us plau, introduïu un NIF o NIE vàlid." #: contrib/localflavor/es/forms.py:68 msgid "Invalid checksum for NIF." @@ -2276,7 +2281,7 @@ msgstr "Validació invàlida del CIF." msgid "" "Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX." msgstr "" -"Introdueixi un número de compte bancari vàlid en el format XXXX-XXXX-XX-" +"Introduïu un número de compte bancari vàlid en el format XXXX-XXXX-XX-" "XXXXXXXXXX." #: contrib/localflavor/es/forms.py:143 @@ -2285,17 +2290,17 @@ msgstr "Validació invàlida del número de compte bancari." #: contrib/localflavor/fi/forms.py:28 msgid "Enter a valid Finnish social security number." -msgstr "Introdueixi un número vàlid de la seguretat social finlandesa." +msgstr "Introduïu un número vàlid de la seguretat social finlandesa." #: contrib/localflavor/in_/forms.py:14 msgid "Enter a zip code in the format XXXXXXX." -msgstr "Introdueixi un codi zip en el format XXXXXXX." +msgstr "Introduïu un codi zip en el format XXXXXXX." #: contrib/localflavor/is_/forms.py:17 msgid "" "Enter a valid Icelandic identification number. The format is XXXXXX-XXXX." msgstr "" -"Introdueixi un número de identificació d'Islàndia. El format és XXXXXX-XXXX." +"Introduïu un número de identificació d'Islàndia. El format és XXXXXX-XXXX." #: contrib/localflavor/is_/forms.py:18 msgid "The Icelandic identification number is not valid." @@ -2303,19 +2308,19 @@ msgstr "El número de identificació d'Islàndia no és vàlid." #: contrib/localflavor/it/forms.py:14 msgid "Enter a valid zip code." -msgstr "Introdueixi un codi zip vàlid." +msgstr "Introduïu un codi zip vàlid." #: contrib/localflavor/it/forms.py:43 msgid "Enter a valid Social Security number." -msgstr "Introdueixi un número valid de la Seguretat Social." +msgstr "Introduïu un número valid de la Seguretat Social." #: contrib/localflavor/it/forms.py:68 msgid "Enter a valid VAT number." -msgstr "Introdueixi un número de IVA (VAT) vàlid." +msgstr "Introduïu un número de IVA (VAT) vàlid." #: contrib/localflavor/jp/forms.py:17 msgid "Enter a postal code in the format XXXXXXX or XXX-XXXX." -msgstr "Introdueixi un codi postal en el format XXXXXXX o XX-XXXX." +msgstr "Introduïu un codi postal en el format XXXXXXX o XX-XXXX." #: contrib/localflavor/jp/jp_prefectures.py:4 msgid "Hokkaido" @@ -2716,7 +2721,7 @@ msgstr "Validació invàlida del número d'identificació nacional." #: contrib/localflavor/pl/forms.py:72 msgid "" "Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX." -msgstr "Introdueixi un número NIP en el format XXX-XXX-XX-XX o XX-XX-XXX-XXX." +msgstr "Introduïu un número NIP en el format XXX-XXX-XX-XX o XX-XX-XXX-XXX." #: contrib/localflavor/pl/forms.py:73 msgid "Wrong checksum for the Tax Number (NIP)." @@ -3437,21 +3442,21 @@ msgstr "Gal·les" #: contrib/localflavor/us/forms.py:16 msgid "Enter a zip code in the format XXXXX or XXXXX-XXXX." -msgstr "Introdueixi un codi zip en el format XXXXX o XXXXX-XXXX." +msgstr "Introduïu un codi zip en el format XXXXX o XXXXX-XXXX." #: contrib/localflavor/us/forms.py:54 msgid "Enter a valid U.S. Social Security number in XXX-XX-XXXX format." msgstr "" -"Introdueixi un número vàlid de la Seguretat Social dels E.U.A. en el format " +"Introduïu un número vàlid de la Seguretat Social dels E.U.A. en el format " "XXX-XX-XXXX." #: contrib/localflavor/za/forms.py:20 msgid "Enter a valid South African ID number" -msgstr "Introdueixi un número d'Identitat Sud Africà valid" +msgstr "Introduïu un número d'Identitat Sud Africà valid" #: contrib/localflavor/za/forms.py:54 msgid "Enter a valid South African postal code" -msgstr "Introdueixi un codi postal Sud Africà vàlid." +msgstr "Introduïu un codi postal Sud Africà vàlid." #: contrib/localflavor/za/za_provinces.py:4 msgid "Eastern Cape" @@ -3510,8 +3515,8 @@ msgid "" "This can be either an absolute path (as above) or a full URL starting with " "'http://'." msgstr "" -"Això pot ser bé una ruta absoluta (com a dalt) o una URL completa que comenci " -"per http:// ." +"Això pot ser bé una ruta absoluta (com a dalt) o una URL completa que " +"comenci per http:// ." #: contrib/redirects/models.py:13 msgid "redirect" @@ -3584,15 +3589,15 @@ msgstr "No es permeten minúscules aquí." #: core/validators.py:95 msgid "Enter only digits separated by commas." -msgstr "Introdueixi només dígits separats per comes." +msgstr "Introduïu només dígits separats per comes." #: core/validators.py:107 msgid "Enter valid e-mail addresses separated by commas." -msgstr "Introdueixi adreces de correu electrònic vàlides separades per comes." +msgstr "Introduïu adreces de correu electrònic vàlides separades per comes." #: core/validators.py:111 msgid "Please enter a valid IP address." -msgstr "Si el plau, introdueixi una adreça IP vàlida." +msgstr "Si el plau, introduïu una adreça IP vàlida." #: core/validators.py:115 msgid "Empty values are not allowed here." @@ -3608,7 +3613,7 @@ msgstr "Aquest valor no pot contenir només dígits." #: core/validators.py:128 newforms/fields.py:152 msgid "Enter a whole number." -msgstr "Introdueixi un número sencer." +msgstr "Introduïu un número sencer." #: core/validators.py:132 msgid "Only alphabetical characters are allowed here." @@ -3625,19 +3630,19 @@ msgstr "Data invàlida: %s" #: core/validators.py:156 db/models/fields/__init__.py:527 msgid "Enter a valid date in YYYY-MM-DD format." -msgstr "Introdueixi una data vàlida en el forma AAAA-MM-DD." +msgstr "Introduïu una data vàlida en el forma AAAA-MM-DD." #: core/validators.py:161 msgid "Enter a valid time in HH:MM format." -msgstr "Introdueixi una hora vàlida en el format HH:MM." +msgstr "Introduïu una hora vàlida en el format HH:MM." #: core/validators.py:165 db/models/fields/__init__.py:604 msgid "Enter a valid date/time in YYYY-MM-DD HH:MM format." -msgstr "Introdueixi un data/hora vàlida en format YYYY-MM-DD HH:MM." +msgstr "Introduïu un data/hora vàlida en format YYYY-MM-DD HH:MM." #: core/validators.py:170 newforms/fields.py:403 msgid "Enter a valid e-mail address." -msgstr "Introdueixi una adreça de correu vàlida." +msgstr "Introduïu una adreça de correu vàlida." #: core/validators.py:182 core/validators.py:474 newforms/fields.py:433 #: oldforms/__init__.py:687 @@ -3650,20 +3655,20 @@ msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -"Envii una imatge vàlida. El fitxer que ha enviat no era una imatge o estaba " +"Envieu una imatge vàlida. El fitxer que heu enviat no era una imatge o estava " "corrupte." #: core/validators.py:200 #, python-format msgid "The URL %s does not point to a valid image." -msgstr "La URL %s no apunta una imatge vàlida." +msgstr "La URL %s no apunta a una imatge vàlida." #: core/validators.py:204 #, python-format msgid "Phone numbers must be in XXX-XXX-XXXX format. \"%s\" is invalid." msgstr "" -"Els números de telèfon han de guardar-se en el format XXX-XXX-XXXX. \"%s\" no " -"és vàlid." +"Els números de telèfon han de guardar-se en el format XXX-XXX-XXXX. \"%s\" " +"no és vàlid." #: core/validators.py:212 #, python-format @@ -3696,7 +3701,7 @@ msgstr "URL invàlida: %s" #: core/validators.py:259 core/validators.py:261 #, python-format msgid "The URL %s is a broken link." -msgstr "La URL %sés un enllaç trencat." +msgstr "La URL %s és un enllaç trencat." #: core/validators.py:267 msgid "Enter a valid U.S. state abbreviation." @@ -3706,8 +3711,8 @@ msgstr "Introdueixi una abreviatura vàlida d'estat dels E.U.A.." #, python-format msgid "Watch your mouth! The word %s is not allowed here." msgid_plural "Watch your mouth! The words %s are not allowed here." -msgstr[0] "Vigili amb el llenguatge! Aquí no s'admet la paraula: %s." -msgstr[1] "Vigili amb el llenguatge! Aquí no s'admeten les paraules: %s." +msgstr[0] "Vigileu amb el vostre llenguatge! Aquí no s'admet la paraula: %s." +msgstr[1] "Vigileu amb el vostre llenguatge! Aquí no s'admeten les paraules: %s." #: core/validators.py:288 #, python-format @@ -3716,11 +3721,11 @@ msgstr "Aquest camp ha de concordar amb el camp '%s'." #: core/validators.py:307 msgid "Please enter something for at least one field." -msgstr "Si us plau, introdueixi alguna cosa almenys en un camp." +msgstr "Si us plau, introduïu alguna cosa almenys en un camp." #: core/validators.py:316 core/validators.py:327 msgid "Please enter both fields or leave them both empty." -msgstr "Si us plau, ompli els dos camps o deixi'ls tots dos en blanc." +msgstr "Si us plau, ompliu els dos camps o deixeu-los tots dos en blanc." #: core/validators.py:335 #, python-format @@ -3758,7 +3763,7 @@ msgstr "Aquest valor ha de ser una potència de %s." #: core/validators.py:437 msgid "Please enter a valid decimal number." -msgstr "Si us plau, introdueixi un número decimal vàlid." +msgstr "Si us plau, introduïu un número decimal vàlid." #: core/validators.py:444 #, python-format @@ -3766,9 +3771,9 @@ msgid "Please enter a valid decimal number with at most %s total digit." msgid_plural "" "Please enter a valid decimal number with at most %s total digits." msgstr[0] "" -"Si us plau, introdueixi un número decimal vàlid de com a màxim %s digit." +"Si us plau, introduïu un número decimal vàlid de com a màxim %s digit." msgstr[1] "" -"Si us plau, introdueixi un número decimal vàlid de com a màxim %s digits." +"Si us plau, introduïu un número decimal vàlid de com a màxim %s digits." #: core/validators.py:447 #, python-format @@ -3777,10 +3782,10 @@ msgid "" msgid_plural "" "Please enter a valid decimal number with a whole part of at most %s digits." msgstr[0] "" -"Si us plau, introdueixi un número decimal vàlid que la seva part sencera " +"Si us plau, introduïu un número decimal vàlid que la seva part sencera " "sigui de com a màxim %s digit." msgstr[1] "" -"Si us plau, introdueixi un número decimal vàlid que la seva part sencera " +"Si us plau, introduïu un número decimal vàlid que la seva part sencera " "sigui de com a màxim %s digits." #: core/validators.py:450 @@ -3789,25 +3794,25 @@ msgid "Please enter a valid decimal number with at most %s decimal place." msgid_plural "" "Please enter a valid decimal number with at most %s decimal places." msgstr[0] "" -"Si us plau, introdueixi un número decimal vàlid de com a màxim %s posició " +"Si us plau, introduïu un número decimal vàlid de com a màxim %s posició " "decimal." msgstr[1] "" -"Si us plau, introdueixi un número decimal vàlid de com a màxim %s posicions " +"Si us plau, introduïu un número decimal vàlid de com a màxim %s posicions " "decimals." #: core/validators.py:458 msgid "Please enter a valid floating point number." -msgstr "Si us plau, introdueixi un número amb punt de coma flotant vàlid." +msgstr "Si us plau, introduïu un número amb punt de coma flotant vàlid." #: core/validators.py:467 #, python-format msgid "Make sure your uploaded file is at least %s bytes big." -msgstr "Asseguris de que el fitxer que ha enviat té, com a mínim, %s bytes." +msgstr "Assegureu-vos de que el fitxer que heu enviat té, com a mínim, %s bytes." #: core/validators.py:468 #, python-format msgid "Make sure your uploaded file is at most %s bytes big." -msgstr "Asseguris de que el fitxer que ha enviat té, com a màxim %s bytes." +msgstr "Assegureu-vos de que el fitxer que heu enviat té, com a màxim %s bytes." #: core/validators.py:485 msgid "The format for this field is wrong." @@ -3836,7 +3841,7 @@ msgid "" "Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with " "\"%(start)s\".)" msgstr "" -"Si us plau, tanqui l'etiqueta %(tag)s des de la línia %(line)s. (La línia " +"Si us plau, tanqueu l'etiqueta %(tag)s des de la línia %(line)s. (La línia " "comença amb \"%(start)s\".)" #: core/validators.py:576 @@ -3918,7 +3923,7 @@ msgstr "Aquest valor ha de ser un número decimal." #: db/models/fields/__init__.py:779 msgid "Enter a valid filename." -msgstr "Introdueixi un nom de fitxer vàlid." +msgstr "Introduïu un nom de fitxer vàlid." #: db/models/fields/__init__.py:960 msgid "This value must be either None, True or False." @@ -3927,7 +3932,7 @@ msgstr "Aquest valor ha de ser None (Cap), True (Veritat) o False (Fals)" #: db/models/fields/related.py:93 #, python-format msgid "Please enter a valid %s." -msgstr "Si us plau, introdueixi un %s vàlid." +msgstr "Si us plau, introduïu un %s vàlid." #: db/models/fields/related.py:701 msgid "Separate multiple IDs with commas." @@ -3936,7 +3941,9 @@ msgstr "Separi múltiples IDs amb comes." #: db/models/fields/related.py:703 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "Premeu la tecla \"Control\" -o \"Command\" en un Mac- per seleccionar més d'un valor." +msgstr "" +"Premeu la tecla \"Control\" -o \"Command\" en un Mac- per seleccionar més " +"d'un valor." #: db/models/fields/related.py:750 #, python-format @@ -3944,28 +3951,28 @@ msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" "Please enter valid %(self)s IDs. The values %(value)r are invalid." msgstr[0] "" -"Si us plau, introdueixi els IDs de %(self)s vàlids. El valor %(value)r és " +"Si us plau, introduïu els IDs de %(self)s vàlids. El valor %(value)r és " "invàlid." msgstr[1] "" -"Si us plau, introdueixi IDs de %(self)s vàlids. Els valors %(value)r són " +"Si us plau, introduïu IDs de %(self)s vàlids. Els valors %(value)r són " "invàlids." #: newforms/fields.py:47 msgid "Enter a valid value." -msgstr "Introdueixi un valor vàlid." +msgstr "Introduïu un valor vàlid." #: newforms/fields.py:124 #, python-format msgid "Ensure this value has at most %(max)d characters (it has %(length)d)." msgstr "" -"Asseguris de que el valor té com a màxim %(max)d caràcters (en té %(length)" +"Assegureu-vos de que el valor té com a màxim %(max)d caràcters (en té %(length)" "d)." #: newforms/fields.py:125 #, python-format msgid "Ensure this value has at least %(min)d characters (it has %(length)d)." msgstr "" -"Asseguris de que el valor té com a mínim %(min)d caràcters (en té %(length)" +"Assegureu-vos de que el valor té com a mínim %(min)d caràcters (en té %(length)" "d)." #: newforms/fields.py:153 newforms/fields.py:182 newforms/fields.py:211 @@ -3976,38 +3983,38 @@ msgstr "Aquest valor ha de ser menor o igual a %s." #: newforms/fields.py:154 newforms/fields.py:183 newforms/fields.py:212 #, python-format msgid "Ensure this value is greater than or equal to %s." -msgstr "Asseguris de que aquest valor sigui superior o igual a %s." +msgstr "Assegureu-vos de que aquest valor sigui superior o igual a %s." #: newforms/fields.py:181 newforms/fields.py:210 msgid "Enter a number." -msgstr "Introdueixi un número." +msgstr "Introduïu un número." #: newforms/fields.py:213 #, python-format msgid "Ensure that there are no more than %s digits in total." -msgstr "Asseguris de que no hi ha més de %s dígits en total." +msgstr "Assegureu-vos de que no hi ha més de %s dígits en total." #: newforms/fields.py:214 #, python-format msgid "Ensure that there are no more than %s decimal places." -msgstr "Asseguris de que no hi ha més de %s decimals." +msgstr "Assegureu-vos de que no hi ha més de %s decimals." #: newforms/fields.py:215 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." -msgstr "Asseguris de que no hi ha més de %s dígits decimals." +msgstr "Assegureu-vos de que no hi ha més de %s dígits decimals." #: newforms/fields.py:263 newforms/fields.py:751 msgid "Enter a valid date." -msgstr "Introdueixi una data vàlida." +msgstr "Introduïu una data vàlida." #: newforms/fields.py:296 newforms/fields.py:752 msgid "Enter a valid time." -msgstr "Introdueixi una hora vàlida." +msgstr "Introduïu una hora vàlida." #: newforms/fields.py:335 msgid "Enter a valid date/time." -msgstr "Introdueixi una data/hora vàlides." +msgstr "Introduïu una data/hora vàlides." #: newforms/fields.py:434 msgid "No file was submitted." @@ -4019,7 +4026,7 @@ msgstr "El fitxer enviat està buit." #: newforms/fields.py:497 msgid "Enter a valid URL." -msgstr "Introdueixi una URL vàlida." +msgstr "Introduïu una URL vàlida." #: newforms/fields.py:498 msgid "This URL appears to be a broken link." @@ -4028,32 +4035,32 @@ msgstr "Aquesta URL sembla ser un enllaç trencat." #: newforms/fields.py:560 newforms/models.py:299 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "" -"Esculli una opció vàlida; Aquesta opció no és una de les opcions disponibles." +"Escolliy una opció vàlida; Aquesta opció no és una de les opcions disponibles." #: newforms/fields.py:599 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." -msgstr "Esculli una opció vàlida. %(value)s no és una de les opcions vàlides." +msgstr "Esculliu una opció vàlida. %(value)s no és una de les opcions vàlides." #: newforms/fields.py:600 newforms/fields.py:662 newforms/models.py:371 msgid "Enter a list of values." -msgstr "Introdueixi una llista de valors." +msgstr "Introduïu una llista de valors." #: newforms/fields.py:780 msgid "Enter a valid IPv4 address." -msgstr "Introdueixi una adreça IPv4 vàlida." +msgstr "Introduïu una adreça IPv4 vàlida." #: newforms/models.py:372 #, python-format msgid "Select a valid choice. %s is not one of the available choices." -msgstr "Esculli una opció vàlida; %s' no és una de les opcions vàlides." +msgstr "Escolliu una opció vàlida; %s' no és una de les opcions vàlides." #: oldforms/__init__.py:409 #, python-format msgid "Ensure your text is less than %s character." msgid_plural "Ensure your text is less than %s characters." -msgstr[0] "Asseguris de que el seu text té menys de %s caràcter." -msgstr[1] "Asseguris de que el seu text té menys de %s caràcters." +msgstr[0] "Assegureu-vos de que el seu text té menys de %s caràcter." +msgstr[1] "Assegureu-vos de que el seu text té menys de %s caràcters." #: oldforms/__init__.py:414 msgid "Line breaks are not allowed here." @@ -4062,19 +4069,19 @@ msgstr "No es permeten salts de línia." #: oldforms/__init__.py:512 oldforms/__init__.py:586 oldforms/__init__.py:625 #, python-format msgid "Select a valid choice; '%(data)s' is not in %(choices)s." -msgstr "Esculli una opció vàlida; %(data)s' no està dintre de %(choices)s." +msgstr "Escolliu una opció vàlida; %(data)s' no està dintre de %(choices)s." #: oldforms/__init__.py:745 msgid "Enter a whole number between -32,768 and 32,767." -msgstr "Introdueixi un enter entre -32,768 i 32,767." +msgstr "Introduïu un enter entre -32,768 i 32,767." #: oldforms/__init__.py:755 msgid "Enter a positive number." -msgstr "Introdueixi un número positiu." +msgstr "Introduïu un número positiu." #: oldforms/__init__.py:765 msgid "Enter a whole number between 0 and 32,767." -msgstr "Introdueixi un número entre 0 i 32,767." +msgstr "Introduïu un número entre 0 i 32,767." #: template/defaultfilters.py:698 msgid "yes,no,maybe" @@ -4383,18 +4390,21 @@ msgstr "j de/d' F del Y" #: views/generic/create_update.py:43 #, python-format msgid "The %(verbose_name)s was created successfully." -msgstr "El/La %(verbose_name)s s'ha creat amb èxit." +msgstr "El/la %(verbose_name)s s'ha creat amb èxit." #: views/generic/create_update.py:117 #, python-format msgid "The %(verbose_name)s was updated successfully." -msgstr "El/La %(verbose_name)s s'ha actualtzat amb èxit." +msgstr "El/la %(verbose_name)s s'ha actualtzat amb èxit." #: views/generic/create_update.py:184 #, python-format msgid "The %(verbose_name)s was deleted." msgstr "El %(verbose_name)s s'ha eliminat." +#~ msgid "Brazilian" +#~ msgstr "Brasileny" + #~ msgid "Gaeilge" #~ msgstr "Gaeilge" diff --git a/django/conf/locale/fr/LC_MESSAGES/django.mo b/django/conf/locale/fr/LC_MESSAGES/django.mo index efefb0bb6f..60b760f7a8 100644 Binary files a/django/conf/locale/fr/LC_MESSAGES/django.mo and b/django/conf/locale/fr/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/fr/LC_MESSAGES/django.po b/django/conf/locale/fr/LC_MESSAGES/django.po index 55387f4bd3..3e133e8ca7 100644 --- a/django/conf/locale/fr/LC_MESSAGES/django.po +++ b/django/conf/locale/fr/LC_MESSAGES/django.po @@ -4277,23 +4277,33 @@ msgstr "ou" #: utils/timesince.py:21 msgid "year" -msgstr "année" +msgid_plural "years" +msgstr[0] "année" +msgstr[1] "années" #: utils/timesince.py:22 msgid "month" -msgstr "mois" +msgid_plural "months" +msgstr[0] "mois" +msgstr[1] "mois" #: utils/timesince.py:23 msgid "week" -msgstr "semaine" +msgid_plural "weeks" +msgstr[0] "semaine" +msgstr[1] "semaines" #: utils/timesince.py:24 msgid "day" -msgstr "journée" +msgid_plural "days" +msgstr[0] "journée" +msgstr[1] "journées" #: utils/timesince.py:25 msgid "hour" -msgstr "heure" +msgid_plural "hours" +msgstr[0] "heure" +msgstr[1] "heures" #: utils/timesince.py:26 msgid "minute" diff --git a/django/conf/locale/ro/LC_MESSAGES/django.mo b/django/conf/locale/ro/LC_MESSAGES/django.mo index 7742ece5b1..c4ca6e660b 100644 Binary files a/django/conf/locale/ro/LC_MESSAGES/django.mo and b/django/conf/locale/ro/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ro/LC_MESSAGES/django.po b/django/conf/locale/ro/LC_MESSAGES/django.po index e92f4f448f..cdbe7a49fe 100644 --- a/django/conf/locale/ro/LC_MESSAGES/django.po +++ b/django/conf/locale/ro/LC_MESSAGES/django.po @@ -1,982 +1,565 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , 2005. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2006-05-16 10:12+0200\n" -"PO-Revision-Date: 2005-11-08 19:06+GMT+2\n" -"Last-Translator: Tiberiu Micu \n" +"POT-Creation-Date: 2008-04-25 14:44+0300\n" +"PO-Revision-Date: 2008-04-25 21:44+0200\n" +"Last-Translator: \n" "Language-Team: Romanian \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: contrib/comments/models.py:67 contrib/comments/models.py:166 -#, fuzzy -msgid "object ID" -msgstr "id obiect" +#: .\conf\global_settings.py:39 +msgid "Arabic" +msgstr "Araba" -#: contrib/comments/models.py:68 -msgid "headline" -msgstr "titlu" +#: .\conf\global_settings.py:40 +msgid "Bengali" +msgstr "Bengaleza" -#: contrib/comments/models.py:69 contrib/comments/models.py:90 -#: contrib/comments/models.py:167 -#, fuzzy -msgid "comment" -msgstr "comentariu" +#: .\conf\global_settings.py:41 +msgid "Bulgarian" +msgstr "Bulgara" -#: contrib/comments/models.py:70 -msgid "rating #1" -msgstr "" +#: .\conf\global_settings.py:42 +msgid "Catalan" +msgstr "Catalana" -#: contrib/comments/models.py:71 -msgid "rating #2" -msgstr "" +#: .\conf\global_settings.py:43 +msgid "Czech" +msgstr "Cehă" -#: contrib/comments/models.py:72 -msgid "rating #3" -msgstr "" +#: .\conf\global_settings.py:44 +msgid "Welsh" +msgstr "Galeza" -#: contrib/comments/models.py:73 -msgid "rating #4" -msgstr "" +#: .\conf\global_settings.py:45 +msgid "Danish" +msgstr "Daneza" -#: contrib/comments/models.py:74 -msgid "rating #5" -msgstr "" +#: .\conf\global_settings.py:46 +msgid "German" +msgstr "Germană" -#: contrib/comments/models.py:75 -msgid "rating #6" -msgstr "" +#: .\conf\global_settings.py:47 +msgid "Greek" +msgstr "Greaca" -#: contrib/comments/models.py:76 -msgid "rating #7" -msgstr "" +#: .\conf\global_settings.py:48 +msgid "English" +msgstr "Engleză" -#: contrib/comments/models.py:77 -msgid "rating #8" -msgstr "" +#: .\conf\global_settings.py:49 +msgid "Spanish" +msgstr "Spaniolă" -#: contrib/comments/models.py:82 -msgid "is valid rating" -msgstr "" +#: .\conf\global_settings.py:50 +msgid "Argentinean Spanish" +msgstr "Spaniola argentiniana" -#: contrib/comments/models.py:83 contrib/comments/models.py:169 -msgid "date/time submitted" -msgstr "data/ora crearii" +#: .\conf\global_settings.py:51 +msgid "Basque" +msgstr "Basca" -#: contrib/comments/models.py:84 contrib/comments/models.py:170 -msgid "is public" -msgstr "public" +#: .\conf\global_settings.py:52 +msgid "Persian" +msgstr "Persana" -#: contrib/comments/models.py:85 contrib/admin/views/doc.py:289 -msgid "IP address" -msgstr "adresa ip" +#: .\conf\global_settings.py:53 +msgid "Finnish" +msgstr "Finlandeza" -#: contrib/comments/models.py:86 -msgid "is removed" -msgstr "sters" +#: .\conf\global_settings.py:54 +msgid "French" +msgstr "Franceză" -#: contrib/comments/models.py:86 -msgid "" -"Check this box if the comment is inappropriate. A \"This comment has been " -"removed\" message will be displayed instead." -msgstr "" +#: .\conf\global_settings.py:55 +msgid "Irish" +msgstr "Irlandeza" -#: contrib/comments/models.py:91 -#, fuzzy -msgid "comments" -msgstr "conţinut" +#: .\conf\global_settings.py:56 +msgid "Galician" +msgstr "Galiciană" -#: contrib/comments/models.py:131 contrib/comments/models.py:207 -#, fuzzy -msgid "Content object" -msgstr "tip conţinut" +#: .\conf\global_settings.py:57 +msgid "Hungarian" +msgstr "Ungara" -#: contrib/comments/models.py:159 -#, python-format -msgid "" -"Posted by %(user)s at %(date)s\n" -"\n" -"%(comment)s\n" -"\n" -"http://%(domain)s%(url)s" -msgstr "" +#: .\conf\global_settings.py:58 +msgid "Hebrew" +msgstr "Ebraica" -#: contrib/comments/models.py:168 -#, fuzzy -msgid "person's name" -msgstr "Prenume" +#: .\conf\global_settings.py:59 +msgid "Croatian" +msgstr "Croata" -#: contrib/comments/models.py:171 -#, fuzzy -msgid "ip address" -msgstr "adresa email" +#: .\conf\global_settings.py:60 +msgid "Icelandic" +msgstr "Islandeza" -#: contrib/comments/models.py:173 -msgid "approved by staff" -msgstr "aprobat de echipa" +#: .\conf\global_settings.py:61 +msgid "Italian" +msgstr "Italiană" -#: contrib/comments/models.py:176 -#, fuzzy -msgid "free comment" -msgstr "permite comentarii" +#: .\conf\global_settings.py:62 +msgid "Japanese" +msgstr "Japoneza" -#: contrib/comments/models.py:177 -#, fuzzy -msgid "free comments" -msgstr "permite comentarii" +#: .\conf\global_settings.py:63 +msgid "Georgian" +msgstr "Georgiana" -#: contrib/comments/models.py:233 -msgid "score" -msgstr "" +#: .\conf\global_settings.py:64 +msgid "Korean" +msgstr "Koreana" -#: contrib/comments/models.py:234 -#, fuzzy -msgid "score date" -msgstr "data expirare" +#: .\conf\global_settings.py:65 +msgid "Khmer" +msgstr "Khmera" -#: contrib/comments/models.py:237 -msgid "karma score" -msgstr "" +#: .\conf\global_settings.py:66 +msgid "Kannada" +msgstr "Limba kannada" -#: contrib/comments/models.py:238 -msgid "karma scores" -msgstr "" +#: .\conf\global_settings.py:67 +msgid "Latvian" +msgstr "Letona" -#: contrib/comments/models.py:242 -#, python-format -msgid "%(score)d rating by %(user)s" -msgstr "" +#: .\conf\global_settings.py:68 +msgid "Macedonian" +msgstr "Macedoneana" -#: contrib/comments/models.py:258 -#, python-format -msgid "" -"This comment was flagged by %(user)s:\n" -"\n" -"%(text)s" -msgstr "" +#: .\conf\global_settings.py:69 +msgid "Dutch" +msgstr "Olandeza" -#: contrib/comments/models.py:265 -#, fuzzy -msgid "flag date" -msgstr "pagina plată" +#: .\conf\global_settings.py:70 +msgid "Norwegian" +msgstr "Norvegiană" -#: contrib/comments/models.py:268 -#, fuzzy -msgid "user flag" -msgstr "Utilizator" +#: .\conf\global_settings.py:71 +msgid "Polish" +msgstr "Poloneza" -#: contrib/comments/models.py:269 -#, fuzzy -msgid "user flags" -msgstr "Utilizatori" +#: .\conf\global_settings.py:72 +msgid "Portugese" +msgstr "Portugheza" -#: contrib/comments/models.py:273 -#, python-format -msgid "Flag by %r" -msgstr "" +#: .\conf\global_settings.py:73 +msgid "Brazilian Portuguese" +msgstr "Portugheza braziliana" -#: contrib/comments/models.py:278 -#, fuzzy -msgid "deletion date" -msgstr "date sesiune" +#: .\conf\global_settings.py:74 +msgid "Romanian" +msgstr "Romana" -#: contrib/comments/models.py:280 -msgid "moderator deletion" -msgstr "" +#: .\conf\global_settings.py:75 +msgid "Russian" +msgstr "Rusă" -#: contrib/comments/models.py:281 -msgid "moderator deletions" -msgstr "" +#: .\conf\global_settings.py:76 +msgid "Slovak" +msgstr "Slovaca" -#: contrib/comments/models.py:285 -#, python-format -msgid "Moderator deletion by %r" -msgstr "" +#: .\conf\global_settings.py:77 +msgid "Slovenian" +msgstr "Slovena" -#: contrib/comments/views/karma.py:19 -msgid "Anonymous users cannot vote" -msgstr "" +#: .\conf\global_settings.py:78 +msgid "Serbian" +msgstr "Sîrbă" -#: contrib/comments/views/karma.py:23 -#, fuzzy -msgid "Invalid comment ID" -msgstr "permite comentarii" +#: .\conf\global_settings.py:79 +msgid "Swedish" +msgstr "Suedeza" -#: contrib/comments/views/karma.py:25 -msgid "No voting for yourself" -msgstr "" +#: .\conf\global_settings.py:80 +msgid "Tamil" +msgstr "Limba tamila" -#: contrib/comments/views/comments.py:28 -msgid "" -"This rating is required because you've entered at least one other rating." -msgstr "" +#: .\conf\global_settings.py:81 +msgid "Telugu" +msgstr "Limba telugu" -#: contrib/comments/views/comments.py:112 -#, python-format -msgid "" -"This comment was posted by a user who has posted fewer than %(count)s " -"comment:\n" -"\n" -"%(text)s" -msgid_plural "" -"This comment was posted by a user who has posted fewer than %(count)s " -"comments:\n" -"\n" -"%(text)s" -msgstr[0] "" -msgstr[1] "" +#: .\conf\global_settings.py:82 +msgid "Turkish" +msgstr "Turca" -#: contrib/comments/views/comments.py:117 -#, python-format -msgid "" -"This comment was posted by a sketchy user:\n" -"\n" -"%(text)s" -msgstr "" +#: .\conf\global_settings.py:83 +msgid "Ukrainian" +msgstr "Ucraineana" -#: contrib/comments/views/comments.py:189 -#: contrib/comments/views/comments.py:280 -msgid "Only POSTs are allowed" -msgstr "" +#: .\conf\global_settings.py:84 +msgid "Simplified Chinese" +msgstr "Chineză simplificată" -#: contrib/comments/views/comments.py:193 -#: contrib/comments/views/comments.py:284 -msgid "One or more of the required fields wasn't submitted" -msgstr "" +#: .\conf\global_settings.py:85 +msgid "Traditional Chinese" +msgstr "Chineza traditionala" -#: contrib/comments/views/comments.py:197 -#: contrib/comments/views/comments.py:286 -msgid "Somebody tampered with the comment form (security violation)" -msgstr "" - -#: contrib/comments/views/comments.py:207 -#: contrib/comments/views/comments.py:292 -msgid "" -"The comment form had an invalid 'target' parameter -- the object ID was " -"invalid" -msgstr "" - -#: contrib/comments/views/comments.py:257 -#: contrib/comments/views/comments.py:321 -msgid "The comment form didn't provide either 'preview' or 'post'" -msgstr "" - -#: contrib/comments/templates/comments/form.html:6 -#: contrib/comments/templates/comments/form.html:8 -#: contrib/admin/templates/admin/login.html:17 -msgid "Username:" -msgstr "Utilizator:" - -#: contrib/comments/templates/comments/form.html:6 -#: contrib/admin/templates/admin/login.html:20 -msgid "Password:" -msgstr "Parola:" - -#: contrib/comments/templates/comments/form.html:6 -msgid "Forgotten your password?" -msgstr "Ai uitat parola?" - -#: contrib/comments/templates/comments/form.html:8 -#: contrib/admin/templates/admin/object_history.html:3 -#: contrib/admin/templates/admin/change_list.html:5 -#: contrib/admin/templates/admin/base.html:23 -#: contrib/admin/templates/admin/delete_confirmation.html:3 -#: contrib/admin/templates/admin/change_form.html:10 -#: contrib/admin/templates/registration/password_change_done.html:3 -#: contrib/admin/templates/registration/password_change_form.html:3 -#: contrib/admin/templates/admin_doc/bookmarklets.html:4 -#: contrib/admin/templates/admin_doc/view_detail.html:4 -#: contrib/admin/templates/admin_doc/template_tag_index.html:5 -#: contrib/admin/templates/admin_doc/template_detail.html:4 -#: contrib/admin/templates/admin_doc/template_filter_index.html:5 -#: contrib/admin/templates/admin_doc/missing_docutils.html:4 -#: contrib/admin/templates/admin_doc/view_index.html:5 -#: contrib/admin/templates/admin_doc/model_detail.html:3 -#: contrib/admin/templates/admin_doc/index.html:4 -#: contrib/admin/templates/admin_doc/model_index.html:5 -msgid "Log out" -msgstr "Deautentificare" - -#: contrib/comments/templates/comments/form.html:12 -msgid "Ratings" -msgstr "" - -#: contrib/comments/templates/comments/form.html:12 -#: contrib/comments/templates/comments/form.html:23 -msgid "Required" -msgstr "" - -#: contrib/comments/templates/comments/form.html:12 -#: contrib/comments/templates/comments/form.html:23 -msgid "Optional" -msgstr "" - -#: contrib/comments/templates/comments/form.html:23 -msgid "Post a photo" -msgstr "" - -#: contrib/comments/templates/comments/form.html:27 -#: contrib/comments/templates/comments/freeform.html:5 -#, fuzzy -msgid "Comment:" -msgstr "permite comentarii" - -#: contrib/comments/templates/comments/form.html:32 -#: contrib/comments/templates/comments/freeform.html:9 -#, fuzzy -msgid "Preview comment" -msgstr "permite comentarii" - -#: contrib/comments/templates/comments/freeform.html:4 -msgid "Your name:" -msgstr "numele dumneavoastra" - -#: contrib/admin/filterspecs.py:40 +#: .\contrib\admin\filterspecs.py:44 #, python-format msgid "" "

By %s:

\n" "
    \n" msgstr "" +"

    Dupa %s:

    \n" +"
      \n" -#: contrib/admin/filterspecs.py:70 contrib/admin/filterspecs.py:88 -#: contrib/admin/filterspecs.py:143 +#: .\contrib\admin\filterspecs.py:74 +#: .\contrib\admin\filterspecs.py:92 +#: .\contrib\admin\filterspecs.py:147 +#: .\contrib\admin\filterspecs.py:173 msgid "All" msgstr "tot" -#: contrib/admin/filterspecs.py:109 +#: .\contrib\admin\filterspecs.py:113 msgid "Any date" msgstr "orice data" -#: contrib/admin/filterspecs.py:110 +#: .\contrib\admin\filterspecs.py:114 msgid "Today" msgstr "Astazi" -#: contrib/admin/filterspecs.py:113 +#: .\contrib\admin\filterspecs.py:117 msgid "Past 7 days" msgstr "Ultimele 7 zile" -#: contrib/admin/filterspecs.py:115 +#: .\contrib\admin\filterspecs.py:119 msgid "This month" msgstr "Luna aceasta" -#: contrib/admin/filterspecs.py:117 +#: .\contrib\admin\filterspecs.py:121 msgid "This year" msgstr "Anul acesta" -#: contrib/admin/filterspecs.py:143 +#: .\contrib\admin\filterspecs.py:147 +#: .\newforms\widgets.py:231 +#: .\oldforms\__init__.py:592 msgid "Yes" msgstr "Da" -#: contrib/admin/filterspecs.py:143 +#: .\contrib\admin\filterspecs.py:147 +#: .\newforms\widgets.py:231 +#: .\oldforms\__init__.py:592 msgid "No" msgstr "Nu" -#: contrib/admin/filterspecs.py:150 +#: .\contrib\admin\filterspecs.py:154 +#: .\newforms\widgets.py:231 +#: .\oldforms\__init__.py:592 msgid "Unknown" msgstr "Necunoscut" -#: contrib/admin/models.py:16 +#: .\contrib\admin\models.py:18 msgid "action time" msgstr "timp acţiune" -#: contrib/admin/models.py:19 +#: .\contrib\admin\models.py:21 msgid "object id" msgstr "id obiect" -#: contrib/admin/models.py:20 +#: .\contrib\admin\models.py:22 msgid "object repr" msgstr "repr obiect" -#: contrib/admin/models.py:21 +#: .\contrib\admin\models.py:23 msgid "action flag" msgstr "steguleţ acţiune" -#: contrib/admin/models.py:22 +#: .\contrib\admin\models.py:24 msgid "change message" msgstr "schimbă mesaj" -#: contrib/admin/models.py:25 +#: .\contrib\admin\models.py:27 msgid "log entry" msgstr "intrare log" -#: contrib/admin/models.py:26 +#: .\contrib\admin\models.py:28 msgid "log entries" msgstr "intrări log" -#: contrib/admin/templatetags/admin_list.py:228 -msgid "All dates" -msgstr "Toate datele" - -#: contrib/admin/views/decorators.py:9 contrib/auth/forms.py:36 -#: contrib/auth/forms.py:41 -msgid "" -"Please enter a correct username and password. Note that both fields are case-" -"sensitive." -msgstr "" -"Va rugam sa introduceti username-ul si parola corecta. Aveti grija deoarece" -"casutele sunt case sensitive." - -#: contrib/admin/views/decorators.py:23 -#: contrib/admin/templates/admin/login.html:25 -msgid "Log in" -msgstr "Login" - -#: contrib/admin/views/decorators.py:61 -msgid "" -"Please log in again, because your session has expired. Don't worry: Your " -"submission has been saved." -msgstr "" -"Va rugam sa va inregistrati din nou, deoarece sesiunea a expirat. Nu va " -"faceti griji datele au fost salvate." - -#: contrib/admin/views/decorators.py:68 -msgid "" -"Looks like your browser isn't configured to accept cookies. Please enable " -"cookies, reload this page, and try again." -msgstr "" -"Se pare ca browserul dumneavostra nu este configurat sa accepte cookies. Va " -"rugam sa va setati browserul sa accepte cookies, dati un reload la pagina si " -"incercati din nou." - -#: contrib/admin/views/decorators.py:82 -msgid "Usernames cannot contain the '@' character." -msgstr "Username-ul nu are voie sa contina caracterul '@'." - -#: contrib/admin/views/decorators.py:84 -#, python-format -msgid "Your e-mail address is not your username. Try '%s' instead." -msgstr "" - -#: contrib/admin/views/main.py:226 -#, fuzzy -msgid "Site administration" -msgstr "Administrare Django" - -#: contrib/admin/views/main.py:260 -#, python-format -msgid "The %(name)s \"%(obj)s\" was added successfully." -msgstr "%(name)s \"%(obj)s\" a fost inserat cu succes." - -#: contrib/admin/views/main.py:264 contrib/admin/views/main.py:348 -msgid "You may edit it again below." -msgstr "Va puteti edita datele din nou mai jos." - -#: contrib/admin/views/main.py:272 contrib/admin/views/main.py:357 -#, python-format -msgid "You may add another %s below." -msgstr "Mai puteti adauga un alt %s mai jos." - -#: contrib/admin/views/main.py:290 -#, python-format -msgid "Add %s" -msgstr "Adaugă %s" - -#: contrib/admin/views/main.py:336 -#, python-format -msgid "Added %s." -msgstr "Adaugat %s." - -#: contrib/admin/views/main.py:336 contrib/admin/views/main.py:338 -#: contrib/admin/views/main.py:340 -msgid "and" -msgstr "si" - -#: contrib/admin/views/main.py:338 -#, python-format -msgid "Changed %s." -msgstr "Schimbă %s." - -#: contrib/admin/views/main.py:340 -#, python-format -msgid "Deleted %s." -msgstr "Am sters %s." - -#: contrib/admin/views/main.py:343 -msgid "No fields changed." -msgstr "Nu s-a facut nicio schimbare." - -#: contrib/admin/views/main.py:346 -#, python-format -msgid "The %(name)s \"%(obj)s\" was changed successfully." -msgstr "%(name)s \"%(obj)s\" au fost modificate cu succes." - -#: contrib/admin/views/main.py:354 -#, python-format -msgid "" -"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." -msgstr "" -"%(name)s \"%(obj)s\" au fost adaugate cu succes. Le puteti edita mai jos." - -#: contrib/admin/views/main.py:392 -#, python-format -msgid "Change %s" -msgstr "Schimbă %s" - -#: contrib/admin/views/main.py:470 -#, python-format -msgid "One or more %(fieldname)s in %(name)s: %(obj)s" -msgstr "" - -#: contrib/admin/views/main.py:475 -#, python-format -msgid "One or more %(fieldname)s in %(name)s:" -msgstr "" - -#: contrib/admin/views/main.py:508 -#, python-format -msgid "The %(name)s \"%(obj)s\" was deleted successfully." -msgstr "%(name)s \"%(obj)s\" au fost sterse cu succes." - -#: contrib/admin/views/main.py:511 -msgid "Are you sure?" -msgstr "Sunteti sigur?" - -#: contrib/admin/views/main.py:533 -#, python-format -msgid "Change history: %s" -msgstr "Schimbari facute: %s" - -#: contrib/admin/views/main.py:565 -#, python-format -msgid "Select %s" -msgstr "" - -#: contrib/admin/views/main.py:565 -#, python-format -msgid "Select %s to change" -msgstr "Selecteaza %s pentru schimbare" - -#: contrib/admin/views/doc.py:277 contrib/admin/views/doc.py:286 -#: contrib/admin/views/doc.py:288 contrib/admin/views/doc.py:294 -#: contrib/admin/views/doc.py:295 contrib/admin/views/doc.py:297 -msgid "Integer" -msgstr "Intreg" - -#: contrib/admin/views/doc.py:278 -msgid "Boolean (Either True or False)" -msgstr "" - -#: contrib/admin/views/doc.py:279 contrib/admin/views/doc.py:296 -#, python-format -msgid "String (up to %(maxlength)s)" -msgstr "String (pana la %(maxlength)s caractere)" - -#: contrib/admin/views/doc.py:280 -msgid "Comma-separated integers" -msgstr "" - -#: contrib/admin/views/doc.py:281 -msgid "Date (without time)" -msgstr "Data (fara ora)" - -#: contrib/admin/views/doc.py:282 -msgid "Date (with time)" -msgstr "Data (cu ora)" - -#: contrib/admin/views/doc.py:283 -msgid "E-mail address" -msgstr "Adresa email" - -#: contrib/admin/views/doc.py:284 contrib/admin/views/doc.py:287 -msgid "File path" -msgstr "" - -#: contrib/admin/views/doc.py:285 -#, fuzzy -msgid "Decimal number" -msgstr "Decembrie" - -#: contrib/admin/views/doc.py:291 -msgid "Boolean (Either True, False or None)" -msgstr "" - -#: contrib/admin/views/doc.py:292 -msgid "Relation to parent model" -msgstr "" - -#: contrib/admin/views/doc.py:293 -msgid "Phone number" -msgstr "Numar de telefon" - -#: contrib/admin/views/doc.py:298 -msgid "Text" -msgstr "" - -#: contrib/admin/views/doc.py:299 -msgid "Time" -msgstr "Timp" - -#: contrib/admin/views/doc.py:300 contrib/flatpages/models.py:7 -msgid "URL" -msgstr "URL" - -#: contrib/admin/views/doc.py:301 -msgid "U.S. state (two uppercase letters)" -msgstr "" - -#: contrib/admin/views/doc.py:302 -msgid "XML text" -msgstr "" - -#: contrib/admin/templates/admin/object_history.html:3 -#: contrib/admin/templates/admin/change_list.html:5 -#: contrib/admin/templates/admin/base.html:23 -#: contrib/admin/templates/admin/delete_confirmation.html:3 -#: contrib/admin/templates/admin/change_form.html:10 -#: contrib/admin/templates/registration/password_change_done.html:3 -#: contrib/admin/templates/registration/password_change_form.html:3 -#: contrib/admin/templates/admin_doc/bookmarklets.html:3 -msgid "Documentation" -msgstr "Documentatie" - -#: contrib/admin/templates/admin/object_history.html:3 -#: contrib/admin/templates/admin/change_list.html:5 -#: contrib/admin/templates/admin/base.html:23 -#: contrib/admin/templates/admin/delete_confirmation.html:3 -#: contrib/admin/templates/admin/change_form.html:10 -#: contrib/admin/templates/registration/password_change_done.html:3 -#: contrib/admin/templates/registration/password_change_form.html:3 -#: contrib/admin/templates/admin_doc/bookmarklets.html:4 -#: contrib/admin/templates/admin_doc/view_detail.html:4 -#: contrib/admin/templates/admin_doc/template_tag_index.html:5 -#: contrib/admin/templates/admin_doc/template_detail.html:4 -#: contrib/admin/templates/admin_doc/template_filter_index.html:5 -#: contrib/admin/templates/admin_doc/missing_docutils.html:4 -#: contrib/admin/templates/admin_doc/view_index.html:5 -#: contrib/admin/templates/admin_doc/model_detail.html:3 -#: contrib/admin/templates/admin_doc/index.html:4 -#: contrib/admin/templates/admin_doc/model_index.html:5 -msgid "Change password" -msgstr "Schimbă parola" - -#: contrib/admin/templates/admin/object_history.html:5 -#: contrib/admin/templates/admin/500.html:4 -#: contrib/admin/templates/admin/change_list.html:6 -#: contrib/admin/templates/admin/base.html:28 -#: contrib/admin/templates/admin/delete_confirmation.html:6 -#: contrib/admin/templates/admin/change_form.html:13 -#: contrib/admin/templates/registration/password_change_done.html:4 -#: contrib/admin/templates/registration/password_reset_form.html:4 -#: contrib/admin/templates/registration/logged_out.html:4 -#: contrib/admin/templates/registration/password_reset_done.html:4 -#: contrib/admin/templates/registration/password_change_form.html:4 -#: contrib/admin/templates/admin_doc/bookmarklets.html:3 -msgid "Home" -msgstr "Acasă" - -#: contrib/admin/templates/admin/object_history.html:5 -#: contrib/admin/templates/admin/change_form.html:20 -msgid "History" -msgstr "Istoric" - -#: contrib/admin/templates/admin/object_history.html:18 -msgid "Date/time" -msgstr "Dată/oră" - -#: contrib/admin/templates/admin/object_history.html:19 -msgid "User" -msgstr "Utilizator" - -#: contrib/admin/templates/admin/object_history.html:20 -msgid "Action" -msgstr "Acţiune" - -#: contrib/admin/templates/admin/object_history.html:26 -msgid "DATE_WITH_TIME_FULL" -msgstr "N j, Y, P" - -#: contrib/admin/templates/admin/object_history.html:36 -msgid "" -"This object doesn't have a change history. It probably wasn't added via this " -"admin site." -msgstr "" -"Acest obiect nu are un istoric al schimbărilor. Probabil nu a fost adăugat " -"prinintermediul acestui sit de administrare." - -#: contrib/admin/templates/admin/base_site.html:4 -msgid "Django site admin" -msgstr "Administrare sit Django" - -#: contrib/admin/templates/admin/base_site.html:7 -msgid "Django administration" -msgstr "Administrare Django" - -#: contrib/admin/templates/admin/500.html:4 -msgid "Server error" -msgstr "Eroare de server" - -#: contrib/admin/templates/admin/500.html:6 -msgid "Server error (500)" -msgstr "Eroare de server (500)" - -#: contrib/admin/templates/admin/500.html:9 -msgid "Server Error (500)" -msgstr "Eroare server (500)" - -#: contrib/admin/templates/admin/500.html:10 -msgid "" -"There's been an error. It's been reported to the site administrators via e-" -"mail and should be fixed shortly. Thanks for your patience." -msgstr "" -"A apărut o eroare. Este raportată către administrator via emailşi va fi " -"fixată în scurt timp. Mulţumim pentru înţelegere." - -#: contrib/admin/templates/admin/404.html:4 -#: contrib/admin/templates/admin/404.html:8 +#: .\contrib\admin\templates\admin\404.html.py:4 +#: .\contrib\admin\templates\admin\404.html.py:8 msgid "Page not found" msgstr "Pagină inexistentă" -#: contrib/admin/templates/admin/404.html:10 +#: .\contrib\admin\templates\admin\404.html.py:10 msgid "We're sorry, but the requested page could not be found." msgstr "Ne pare rău, dar pagina cerută nu există." -#: contrib/admin/templates/admin/index.html:17 -#, python-format -msgid "Models available in the %(name)s application." -msgstr "" +#: .\contrib\admin\templates\admin\500.html.py:4 +#: .\contrib\admin\templates\admin\base.html.py:37 +#: .\contrib\admin\templates\admin\change_form.html.py:12 +#: .\contrib\admin\templates\admin\change_list.html.py:5 +#: .\contrib\admin\templates\admin\delete_confirmation.html.py:5 +#: .\contrib\admin\templates\admin\invalid_setup.html.py:4 +#: .\contrib\admin\templates\admin\object_history.html.py:4 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:11 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:3 +#: .\contrib\admin\templates\registration\logged_out.html.py:4 +#: .\contrib\admin\templates\registration\password_change_done.html.py:3 +#: .\contrib\admin\templates\registration\password_change_form.html.py:3 +#: .\contrib\admin\templates\registration\password_reset_done.html.py:4 +#: .\contrib\admin\templates\registration\password_reset_form.html.py:4 +msgid "Home" +msgstr "Acasă" -#: contrib/admin/templates/admin/index.html:28 -#: contrib/admin/templates/admin/change_form.html:15 -msgid "Add" -msgstr "Adaugă" +#: .\contrib\admin\templates\admin\500.html.py:4 +msgid "Server error" +msgstr "Eroare de server" -#: contrib/admin/templates/admin/index.html:34 -msgid "Change" -msgstr "Schimbă" +#: .\contrib\admin\templates\admin\500.html.py:6 +msgid "Server error (500)" +msgstr "Eroare de server (500)" -#: contrib/admin/templates/admin/index.html:44 -msgid "You don't have permission to edit anything." -msgstr "Nu ai drepturi de editare." +#: .\contrib\admin\templates\admin\500.html.py:9 +msgid "Server Error (500)" +msgstr "Eroare server (500)" -#: contrib/admin/templates/admin/index.html:52 -msgid "Recent Actions" -msgstr "Acţiuni Recente" +#: .\contrib\admin\templates\admin\500.html.py:10 +msgid "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." +msgstr "A apărut o eroare. Este raportată către administrator via emailşi va fi fixată în scurt timp. Mulţumim pentru înţelegere." -#: contrib/admin/templates/admin/index.html:53 -msgid "My Actions" -msgstr "Acţiunile Mele" - -#: contrib/admin/templates/admin/index.html:57 -msgid "None available" -msgstr "Indisponibil" - -#: contrib/admin/templates/admin/change_list.html:11 -#, python-format -msgid "Add %(name)s" -msgstr "Adaugă %(name)s" - -#: contrib/admin/templates/admin/login.html:22 -msgid "Have you forgotten your password?" -msgstr "Ai uitat parola?" - -#: contrib/admin/templates/admin/base.html:23 +#: .\contrib\admin\templates\admin\base.html.py:26 msgid "Welcome," msgstr "Bine ai venit," -#: contrib/admin/templates/admin/delete_confirmation.html:9 -#: contrib/admin/templates/admin/submit_line.html:3 -msgid "Delete" -msgstr "Sterge" +#: .\contrib\admin\templates\admin\base.html.py:28 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:3 +msgid "Documentation" +msgstr "Documentatie" -#: contrib/admin/templates/admin/delete_confirmation.html:14 -#, python-format -msgid "" -"Deleting the %(object_name)s '%(object)s' would result in deleting related " -"objects, but your account doesn't have permission to delete the following " -"types of objects:" -msgstr "" -"Ştergînd %(object_name)s '%(object)s' va avea ca rezultat ştergerea şi a " -"obiectelor ce au legătură, dar contul tău nu are permisiunea de a şterge " -"următoarele tipuri de obiecte:" +#: .\contrib\admin\templates\admin\base.html.py:29 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:14 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:45 +msgid "Change password" +msgstr "Schimbă parola" -#: contrib/admin/templates/admin/delete_confirmation.html:21 -#, python-format -msgid "" -"Are you sure you want to delete the %(object_name)s \"%(object)s\"? All of " -"the following related items will be deleted:" -msgstr "" -"Eşti sigur că vrei să ştergi %(object_name)s \"%(object)s\"?Următoarele " -"componente vor fi şterse:" +#: .\contrib\admin\templates\admin\base.html.py:30 +#: .\contrib\comments\templates\comments\form.html.py:6 +msgid "Log out" +msgstr "Deautentificare" -#: contrib/admin/templates/admin/delete_confirmation.html:26 -msgid "Yes, I'm sure" -msgstr "Da, sînt sigur" +#: .\contrib\admin\templates\admin\base_site.html.py:4 +msgid "Django site admin" +msgstr "Administrare sit Django" -#: contrib/admin/templates/admin/filter.html:2 -#, python-format -msgid " By %(title)s " -msgstr " Dupa %(title)s " +#: .\contrib\admin\templates\admin\base_site.html.py:7 +msgid "Django administration" +msgstr "Administrare Django" -#: contrib/admin/templates/admin/search_form.html:8 -msgid "Go" -msgstr "" +#: .\contrib\admin\templates\admin\change_form.html.py:14 +#: .\contrib\admin\templates\admin\index.html.py:28 +msgid "Add" +msgstr "Adaugă" -#: contrib/admin/templates/admin/change_form.html:21 +#: .\contrib\admin\templates\admin\change_form.html.py:20 +#: .\contrib\admin\templates\admin\object_history.html.py:4 +msgid "History" +msgstr "Istoric" + +#: .\contrib\admin\templates\admin\change_form.html.py:21 msgid "View on site" -msgstr "" +msgstr "Vizualizeaza pe site" -#: contrib/admin/templates/admin/change_form.html:30 +#: .\contrib\admin\templates\admin\change_form.html.py:31 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:23 msgid "Please correct the error below." msgid_plural "Please correct the errors below." msgstr[0] "Va rugam sa corectati eroarea de mai jos" msgstr[1] "Va rugam sa corectati erorile de mai jos" -#: contrib/admin/templates/admin/change_form.html:48 +#: .\contrib\admin\templates\admin\change_form.html.py:49 msgid "Ordering" msgstr "Ordonate dupa" -#: contrib/admin/templates/admin/change_form.html:51 +#: .\contrib\admin\templates\admin\change_form.html.py:52 msgid "Order:" msgstr "Ordonare:" -#: contrib/admin/templates/admin/submit_line.html:4 +#: .\contrib\admin\templates\admin\change_list.html.py:11 +#, python-format +msgid "Add %(name)s" +msgstr "Adaugă %(name)s" + +#: .\contrib\admin\templates\admin\delete_confirmation.html.py:8 +#: .\contrib\admin\templates\admin\submit_line.html.py:3 +msgid "Delete" +msgstr "Sterge" + +#: .\contrib\admin\templates\admin\delete_confirmation.html.py:13 +#, python-format +msgid "Deleting the %(object_name)s '%(escaped_object)s' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:" +msgstr "Ştergînd %(object_name)s '%(escaped_object)s' va avea ca rezultat ştergerea şi a obiectelor ce au legătură, dar contul tău nu are permisiunea de a şterge următoarele tipuri de obiecte:" + +#: .\contrib\admin\templates\admin\delete_confirmation.html.py:20 +#, python-format +msgid "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? All of the following related items will be deleted:" +msgstr "Eşti sigur că vrei să ştergi %(object_name)s \"%(escaped_object)s\"? Următoarele componente asociate vor fi şterse:" + +#: .\contrib\admin\templates\admin\delete_confirmation.html.py:25 +msgid "Yes, I'm sure" +msgstr "Da, sînt sigur" + +#: .\contrib\admin\templates\admin\filter.html.py:2 +#, python-format +msgid " By %(filter_title)s " +msgstr "Dupa %(filter_title)s " + +#: .\contrib\admin\templates\admin\filters.html.py:4 +msgid "Filter" +msgstr "Filtru" + +#: .\contrib\admin\templates\admin\index.html.py:17 +#, python-format +msgid "Models available in the %(name)s application." +msgstr "Modele disponibile in applicatia %(name)s" + +#: .\contrib\admin\templates\admin\index.html.py:18 +#, python-format +msgid "%(name)s" +msgstr "%(name)s" + +#: .\contrib\admin\templates\admin\index.html.py:34 +msgid "Change" +msgstr "Schimbă" + +#: .\contrib\admin\templates\admin\index.html.py:44 +msgid "You don't have permission to edit anything." +msgstr "Nu ai drepturi de editare." + +#: .\contrib\admin\templates\admin\index.html.py:52 +msgid "Recent Actions" +msgstr "Acţiuni Recente" + +#: .\contrib\admin\templates\admin\index.html.py:53 +msgid "My Actions" +msgstr "Acţiunile Mele" + +#: .\contrib\admin\templates\admin\index.html.py:57 +msgid "None available" +msgstr "Indisponibil" + +#: .\contrib\admin\templates\admin\invalid_setup.html.py:8 +msgid "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." +msgstr "Exista o problema cu baza de date. Verificati daca tabelele necesare din baza de date au fost create si verificati daca baza de date poate fi citita de utilizatorul potrivit." + +#: .\contrib\admin\templates\admin\login.html.py:17 +#: .\contrib\comments\templates\comments\form.html.py:6 +#: .\contrib\comments\templates\comments\form.html.py:8 +msgid "Username:" +msgstr "Utilizator:" + +#: .\contrib\admin\templates\admin\login.html.py:20 +#: .\contrib\comments\templates\comments\form.html.py:8 +msgid "Password:" +msgstr "Parola:" + +#: .\contrib\admin\templates\admin\login.html.py:25 +#: .\contrib\admin\views\decorators.py:31 +msgid "Log in" +msgstr "Login" + +#: .\contrib\admin\templates\admin\object_history.html.py:17 +msgid "Date/time" +msgstr "Dată/oră" + +#: .\contrib\admin\templates\admin\object_history.html.py:18 +msgid "User" +msgstr "Utilizator" + +#: .\contrib\admin\templates\admin\object_history.html.py:19 +msgid "Action" +msgstr "Acţiune" + +#: .\contrib\admin\templates\admin\object_history.html.py:25 +msgid "DATE_WITH_TIME_FULL" +msgstr "N j, Y, P" + +#: .\contrib\admin\templates\admin\object_history.html.py:35 +msgid "This object doesn't have a change history. It probably wasn't added via this admin site." +msgstr "Acest obiect nu are un istoric al schimbărilor. Probabil nu a fost adăugat prinintermediul acestui sit de administrare." + +#: .\contrib\admin\templates\admin\pagination.html.py:10 +msgid "Show all" +msgstr "Arata tot/toate" + +#: .\contrib\admin\templates\admin\search_form.html.py:8 +msgid "Go" +msgstr "Start" + +#: .\contrib\admin\templates\admin\search_form.html.py:10 +#, python-format +msgid "1 result" +msgid_plural "%(counter)s results" +msgstr[0] "Un rezultat" +msgstr[1] "%(counter)s rezultate" + +#: .\contrib\admin\templates\admin\search_form.html.py:10 +#, python-format +msgid "%(full_result_count)s total" +msgstr "%(full_result_count)s total" + +#: .\contrib\admin\templates\admin\submit_line.html.py:4 msgid "Save as new" msgstr "Salvati ca nou" -#: contrib/admin/templates/admin/submit_line.html:5 +#: .\contrib\admin\templates\admin\submit_line.html.py:5 msgid "Save and add another" msgstr "Salvati si adaugati altul" -#: contrib/admin/templates/admin/submit_line.html:6 +#: .\contrib\admin\templates\admin\submit_line.html.py:6 msgid "Save and continue editing" msgstr "Salvati si continuati" -#: contrib/admin/templates/admin/submit_line.html:7 +#: .\contrib\admin\templates\admin\submit_line.html.py:7 msgid "Save" msgstr "Salveaza" -#: contrib/admin/templates/registration/password_change_done.html:4 -#: contrib/admin/templates/registration/password_change_form.html:4 -#: contrib/admin/templates/registration/password_change_form.html:6 -#: contrib/admin/templates/registration/password_change_form.html:10 -msgid "Password change" -msgstr "Schimbă parola" +#: .\contrib\admin\templates\admin\auth\user\add_form.html.py:6 +msgid "First, enter a username and password. Then, you'll be able to edit more user options." +msgstr "Intai introduceti un nume de utilizator si o parola. Apoi veti putea modifica mai multe optiuni de utilizator." -#: contrib/admin/templates/registration/password_change_done.html:6 -#: contrib/admin/templates/registration/password_change_done.html:10 -msgid "Password change successful" -msgstr "Schimbare reuşită a parolei" +#: .\contrib\admin\templates\admin\auth\user\add_form.html.py:12 +msgid "Username" +msgstr "Utilizator" -#: contrib/admin/templates/registration/password_change_done.html:12 -msgid "Your password was changed." -msgstr "Parola a fost schimbată." +#: .\contrib\admin\templates\admin\auth\user\add_form.html.py:18 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:33 +msgid "Password" +msgstr "Parola" -#: contrib/admin/templates/registration/password_reset_form.html:4 -#: contrib/admin/templates/registration/password_reset_form.html:6 -#: contrib/admin/templates/registration/password_reset_form.html:10 -#: contrib/admin/templates/registration/password_reset_done.html:4 -msgid "Password reset" -msgstr "Resetează parola" +#: .\contrib\admin\templates\admin\auth\user\add_form.html.py:23 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:38 +msgid "Password (again)" +msgstr "Parola (repeta)" -#: contrib/admin/templates/registration/password_reset_form.html:12 -msgid "" -"Forgotten your password? Enter your e-mail address below, and we'll reset " -"your password and e-mail the new one to you." -msgstr "" -"Ai uitat parola? Introdu adresa de email mai jos, iar noi vom reseta parola " -"şi-ţi vom trimite una nouă prin email." +#: .\contrib\admin\templates\admin\auth\user\add_form.html.py:24 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:39 +msgid "Enter the same password as above, for verification." +msgstr "Introduceti parola din nou pentru verificare." -#: contrib/admin/templates/registration/password_reset_form.html:16 -msgid "E-mail address:" -msgstr "Adresa email:" - -#: contrib/admin/templates/registration/password_reset_form.html:16 -msgid "Reset my password" -msgstr "Resetează-mi parola" - -#: contrib/admin/templates/registration/logged_out.html:8 -msgid "Thanks for spending some quality time with the Web site today." -msgstr "Mulţumesc pentru petrecerea folositoare a timpului cu saitul astăzi." - -#: contrib/admin/templates/registration/logged_out.html:10 -msgid "Log in again" -msgstr "Relogare" - -#: contrib/admin/templates/registration/password_reset_done.html:6 -#: contrib/admin/templates/registration/password_reset_done.html:10 -msgid "Password reset successful" -msgstr "Parola resetată cu succes" - -#: contrib/admin/templates/registration/password_reset_done.html:12 -msgid "" -"We've e-mailed a new password to the e-mail address you submitted. You " -"should be receiving it shortly." -msgstr "" -"Am trimis o nouă parolă prin email la adresa furnizată. Ar trebuisă o " -"primeşti în scurt timp." - -#: contrib/admin/templates/registration/password_change_form.html:12 -msgid "" -"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." -msgstr "" -"Introdu te rog vechea parolă, pentru motive de siguranţă, şi apoi tastează " -"noua parolă de două ori aşa încît putem verifica dacă ai tastat corect." - -#: contrib/admin/templates/registration/password_change_form.html:17 -msgid "Old password:" -msgstr "Vechea parolă:" - -#: contrib/admin/templates/registration/password_change_form.html:19 -msgid "New password:" -msgstr "Noua parolă:" - -#: contrib/admin/templates/registration/password_change_form.html:21 -msgid "Confirm password:" -msgstr "Confirmă parola:" - -#: contrib/admin/templates/registration/password_change_form.html:23 -msgid "Change my password" -msgstr "Schimbă-mi parola" - -#: contrib/admin/templates/registration/password_reset_email.html:2 -msgid "You're receiving this e-mail because you requested a password reset" -msgstr "Ai primit acest email pentru că ai cerut o resetare a parolei" - -#: contrib/admin/templates/registration/password_reset_email.html:3 +#: .\contrib\admin\templates\admin\auth\user\change_password.html.py:27 #, python-format -msgid "for your user account at %(site_name)s" -msgstr "pentru contul tău la %(site_name)s" +msgid "Enter a new password for the user %(username)s." +msgstr "Introduceti o parola noua pentru utilizatorul %(username)s." -#: contrib/admin/templates/registration/password_reset_email.html:5 -#, python-format -msgid "Your new password is: %(new_password)s" -msgstr "Noua ta parolă este: %(new_password)s" - -#: contrib/admin/templates/registration/password_reset_email.html:7 -msgid "Feel free to change this password by going to this page:" -msgstr "Poţi schmiba această parolă vizitînd această pagină:" - -#: contrib/admin/templates/registration/password_reset_email.html:11 -msgid "Your username, in case you've forgotten:" -msgstr "Numele tău utilizator, în caz că ai uitat:" - -#: contrib/admin/templates/registration/password_reset_email.html:13 -msgid "Thanks for using our site!" -msgstr "Mulţumesc pentru folosirea saitului nostru!" - -#: contrib/admin/templates/registration/password_reset_email.html:15 -#, python-format -msgid "The %(site_name)s team" -msgstr "Echipa %(site_name)s" - -#: contrib/admin/templates/admin_doc/bookmarklets.html:3 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:3 msgid "Bookmarklets" msgstr "" -#: contrib/admin/templates/admin_doc/bookmarklets.html:5 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:4 msgid "Documentation bookmarklets" msgstr "" -#: contrib/admin/templates/admin_doc/bookmarklets.html:9 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:8 msgid "" "\n" "

      To install bookmarklets, drag the link to your bookmarks\n" @@ -987,721 +570,2985 @@ msgid "" "your computer is \"internal\").

      \n" msgstr "" -#: contrib/admin/templates/admin_doc/bookmarklets.html:19 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:18 msgid "Documentation for this page" -msgstr "" +msgstr "Documentatie pentru aceasta pagina" -#: contrib/admin/templates/admin_doc/bookmarklets.html:20 -msgid "" -"Jumps you from any page to the documentation for the view that generates " -"that page." -msgstr "" +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:19 +msgid "Jumps you from any page to the documentation for the view that generates that page." +msgstr "Te trimite de la orice pagina la documentatia pentru view-ul care genereaza acea pagina." -#: contrib/admin/templates/admin_doc/bookmarklets.html:22 -#, fuzzy +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:21 msgid "Show object ID" -msgstr "id obiect" +msgstr "Arata ID-ul obiectului" -#: contrib/admin/templates/admin_doc/bookmarklets.html:23 -msgid "" -"Shows the content-type and unique ID for pages that represent a single " -"object." -msgstr "" +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:22 +msgid "Shows the content-type and unique ID for pages that represent a single object." +msgstr "Arata tipul de continut si ID-ul unic pentru paginile care reprezinta un singur obiect." -#: contrib/admin/templates/admin_doc/bookmarklets.html:25 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:24 msgid "Edit this object (current window)" -msgstr "" +msgstr "Modifica acest obiect (in aceasta fereastra)" -#: contrib/admin/templates/admin_doc/bookmarklets.html:26 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:25 msgid "Jumps to the admin page for pages that represent a single object." -msgstr "" +msgstr "Sare la pagina de administrare pentru pagini care reprezinta un singur obiect." -#: contrib/admin/templates/admin_doc/bookmarklets.html:28 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:27 msgid "Edit this object (new window)" -msgstr "" +msgstr "Modifica acest obiect (intr-o fereastra noua)" -#: contrib/admin/templates/admin_doc/bookmarklets.html:29 +#: .\contrib\admin\templates\admin_doc\bookmarklets.html.py:28 msgid "As above, but opens the admin page in a new window." -msgstr "" +msgstr "La fel ca deasupra, dar deschide pagina de administrare intr-o fereastra noua" -#: contrib/admin/templates/widget/date_time.html:3 +#: .\contrib\admin\templates\registration\logged_out.html.py:8 +msgid "Thanks for spending some quality time with the Web site today." +msgstr "Mulţumesc pentru petrecerea folositoare a timpului cu saitul astăzi." + +#: .\contrib\admin\templates\registration\logged_out.html.py:10 +msgid "Log in again" +msgstr "Relogare" + +#: .\contrib\admin\templates\registration\password_change_done.html.py:3 +#: .\contrib\admin\templates\registration\password_change_form.html.py:3 +#: .\contrib\admin\templates\registration\password_change_form.html.py:5 +#: .\contrib\admin\templates\registration\password_change_form.html.py:9 +msgid "Password change" +msgstr "Schimbă parola" + +#: .\contrib\admin\templates\registration\password_change_done.html.py:5 +#: .\contrib\admin\templates\registration\password_change_done.html.py:9 +msgid "Password change successful" +msgstr "Schimbare reuşită a parolei" + +#: .\contrib\admin\templates\registration\password_change_done.html.py:11 +msgid "Your password was changed." +msgstr "Parola a fost schimbată." + +#: .\contrib\admin\templates\registration\password_change_form.html.py:11 +msgid "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." +msgstr "Introdu te rog vechea parolă, pentru motive de siguranţă, şi apoi tastează noua parolă de două ori aşa încît putem verifica dacă ai tastat corect." + +#: .\contrib\admin\templates\registration\password_change_form.html.py:16 +msgid "Old password:" +msgstr "Vechea parolă:" + +#: .\contrib\admin\templates\registration\password_change_form.html.py:18 +msgid "New password:" +msgstr "Noua parolă:" + +#: .\contrib\admin\templates\registration\password_change_form.html.py:20 +msgid "Confirm password:" +msgstr "Confirmă parola:" + +#: .\contrib\admin\templates\registration\password_change_form.html.py:22 +msgid "Change my password" +msgstr "Schimbă-mi parola" + +#: .\contrib\admin\templates\registration\password_reset_done.html.py:4 +#: .\contrib\admin\templates\registration\password_reset_form.html.py:4 +#: .\contrib\admin\templates\registration\password_reset_form.html.py:6 +#: .\contrib\admin\templates\registration\password_reset_form.html.py:10 +msgid "Password reset" +msgstr "Resetează parola" + +#: .\contrib\admin\templates\registration\password_reset_done.html.py:6 +#: .\contrib\admin\templates\registration\password_reset_done.html.py:10 +msgid "Password reset successful" +msgstr "Parola resetată cu succes" + +#: .\contrib\admin\templates\registration\password_reset_done.html.py:12 +msgid "We've e-mailed a new password to the e-mail address you submitted. You should be receiving it shortly." +msgstr "Am trimis o nouă parolă prin email la adresa furnizată. Ar trebuisă o primeşti în scurt timp." + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:2 +msgid "You're receiving this e-mail because you requested a password reset" +msgstr "Ai primit acest email pentru că ai cerut o resetare a parolei" + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:3 +#, python-format +msgid "for your user account at %(site_name)s" +msgstr "pentru contul tău la %(site_name)s" + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:5 +#, python-format +msgid "Your new password is: %(new_password)s" +msgstr "Noua ta parolă este: %(new_password)s" + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:7 +msgid "Feel free to change this password by going to this page:" +msgstr "Poţi schmiba această parolă vizitînd această pagină:" + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:11 +msgid "Your username, in case you've forgotten:" +msgstr "Numele tău utilizator, în caz că ai uitat:" + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:13 +msgid "Thanks for using our site!" +msgstr "Mulţumesc pentru folosirea saitului nostru!" + +#: .\contrib\admin\templates\registration\password_reset_email.html.py:15 +#, python-format +msgid "The %(site_name)s team" +msgstr "Echipa %(site_name)s" + +#: .\contrib\admin\templates\registration\password_reset_form.html.py:12 +msgid "Forgotten your password? Enter your e-mail address below, and we'll reset your password and e-mail the new one to you." +msgstr "Ai uitat parola? Introdu adresa de email mai jos, iar noi vom reseta parola şi-ţi vom trimite una nouă prin email." + +#: .\contrib\admin\templates\registration\password_reset_form.html.py:16 +msgid "E-mail address:" +msgstr "Adresa email:" + +#: .\contrib\admin\templates\registration\password_reset_form.html.py:16 +msgid "Reset my password" +msgstr "Resetează-mi parola" + +#: .\contrib\admin\templates\widget\date_time.html.py:3 msgid "Date:" -msgstr "" +msgstr "Data:" -#: contrib/admin/templates/widget/date_time.html:4 +#: .\contrib\admin\templates\widget\date_time.html.py:4 msgid "Time:" -msgstr "" +msgstr "Ora:" -#: contrib/admin/templates/widget/file.html:2 +#: .\contrib\admin\templates\widget\file.html.py:2 msgid "Currently:" -msgstr "" +msgstr "Current:" -#: contrib/admin/templates/widget/file.html:3 -#, fuzzy +#: .\contrib\admin\templates\widget\file.html.py:3 msgid "Change:" -msgstr "Schimbă" +msgstr "Schimbă:" -#: contrib/redirects/models.py:7 -msgid "redirect from" -msgstr "redirectat de la " +#: .\contrib\admin\templatetags\admin_list.py:257 +msgid "All dates" +msgstr "Toate datele" -#: contrib/redirects/models.py:8 -msgid "" -"This should be an absolute path, excluding the domain name. Example: '/" -"events/search/'." -msgstr "" -"Aceasta ar trebui să fie o cale absolută, excluzînd numele de domeniu. " -"Exemplu: '/evenimente/cautare/'." +#: .\contrib\admin\views\auth.py:20 +#: .\contrib\admin\views\main.py:267 +msgid "The %(name)s \"%(obj)s\" was added successfully." +msgstr "%(name)s \"%(obj)s\" a fost inserat cu succes." -#: contrib/redirects/models.py:9 -msgid "redirect to" -msgstr "redirectat la" +#: .\contrib\admin\views\auth.py:25 +#: .\contrib\admin\views\main.py:271 +#: .\contrib\admin\views\main.py:356 +msgid "You may edit it again below." +msgstr "Va puteti edita datele din nou mai jos." -#: contrib/redirects/models.py:10 -msgid "" -"This can be either an absolute path (as above) or a full URL starting with " -"'http://'." -msgstr "" -"Aceasta poate fi o cale absolută (ca mai sus) sau un URL începînd cu " -"'http://'." +#: .\contrib\admin\views\auth.py:31 +msgid "Add user" +msgstr "Adaugă utilizator" -#: contrib/redirects/models.py:12 -msgid "redirect" -msgstr "redirectare" +#: .\contrib\admin\views\auth.py:58 +msgid "Password changed successfully." +msgstr "Schimbare reuşită a parolei" -#: contrib/redirects/models.py:13 -msgid "redirects" -msgstr "redictări" +#: .\contrib\admin\views\auth.py:65 +#, python-format +msgid "Change password: %s" +msgstr "Schimbă parola: %s" -#: contrib/flatpages/models.py:8 -msgid "" -"Example: '/about/contact/'. Make sure to have leading and trailing slashes." -msgstr "" -"Exemplu: '/about/contact'. Asiguraţi-vă că aveţi slash-uri la început şi la " -"sfîrşit." +#: .\contrib\admin\views\decorators.py:17 +#: .\contrib\auth\forms.py:60 +msgid "Please enter a correct username and password. Note that both fields are case-sensitive." +msgstr "Va rugam sa introduceti username-ul si parola corecta. Aveti grija deoarececasutele sunt case sensitive." -#: contrib/flatpages/models.py:9 -msgid "title" -msgstr "titlu" +#: .\contrib\admin\views\decorators.py:69 +msgid "Please log in again, because your session has expired. Don't worry: Your submission has been saved." +msgstr "Va rugam sa va inregistrati din nou, deoarece sesiunea a expirat. Nu va faceti griji datele au fost salvate." -#: contrib/flatpages/models.py:10 -msgid "content" -msgstr "conţinut" +#: .\contrib\admin\views\decorators.py:76 +msgid "Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again." +msgstr "Se pare ca browserul dumneavostra nu este configurat sa accepte cookies. Va rugam sa va setati browserul sa accepte cookies, dati un reload la pagina si incercati din nou." -#: contrib/flatpages/models.py:11 -msgid "enable comments" -msgstr "permite comentarii" +#: .\contrib\admin\views\decorators.py:90 +msgid "Usernames cannot contain the '@' character." +msgstr "Username-ul nu are voie sa contina caracterul '@'." -#: contrib/flatpages/models.py:12 -msgid "template name" -msgstr "nume şablon" +#: .\contrib\admin\views\decorators.py:92 +#, python-format +msgid "Your e-mail address is not your username. Try '%s' instead." +msgstr "Adresa ta de e-mail nu este numele tau de utilizator. Incearca '%s' in schimb." -#: contrib/flatpages/models.py:13 -#, fuzzy -msgid "" -"Example: 'flatpages/contact_page'. If this isn't provided, the system will " -"use 'flatpages/default'." -msgstr "" -"Exemplu: 'flatfiles/pagina_contact'. Dacă aceasta nu există, sistemul va " -"folosi 'flatfiles/default'." +#: .\contrib\admin\views\doc.py:48 +#: .\contrib\admin\views\doc.py:50 +#: .\contrib\admin\views\doc.py:52 +msgid "tag:" +msgstr "tag:" -#: contrib/flatpages/models.py:14 -msgid "registration required" -msgstr "necesită înregistrare" +#: .\contrib\admin\views\doc.py:79 +#: .\contrib\admin\views\doc.py:81 +#: .\contrib\admin\views\doc.py:83 +msgid "filter:" +msgstr "filtru:" -#: contrib/flatpages/models.py:14 -msgid "If this is checked, only logged-in users will be able to view the page." -msgstr "" -"Dacă aceasta este bifată, numai utilizatorii logaţi vor putea vedea pagina." +#: .\contrib\admin\views\doc.py:137 +#: .\contrib\admin\views\doc.py:139 +#: .\contrib\admin\views\doc.py:141 +msgid "view:" +msgstr "view:" -#: contrib/flatpages/models.py:18 -msgid "flat page" -msgstr "pagina plată" +#: .\contrib\admin\views\doc.py:166 +#, python-format +msgid "App %r not found" +msgstr "Applicatia %r inexistenta" -#: contrib/flatpages/models.py:19 -msgid "flat pages" -msgstr "pagini plate" +#: .\contrib\admin\views\doc.py:173 +#, python-format +msgid "Model %(name)r not found in app %(label)r" +msgstr "Modelul %(name)r nu a fost gasit in aplicatia %(label)r" -#: contrib/auth/models.py:13 contrib/auth/models.py:26 +#: .\contrib\admin\views\doc.py:185 +#, python-format +msgid "the related `%(label)s.%(type)s` object" +msgstr "obiectul `%(label)s.%(type)s` asociat" + +#: .\contrib\admin\views\doc.py:185 +#: .\contrib\admin\views\doc.py:207 +#: .\contrib\admin\views\doc.py:221 +#: .\contrib\admin\views\doc.py:226 +msgid "model:" +msgstr "model:" + +#: .\contrib\admin\views\doc.py:216 +#, python-format +msgid "related `%(label)s.%(name)s` objects" +msgstr "obiectele `%(label)s.%(name)s` asociate" + +#: .\contrib\admin\views\doc.py:221 +#, python-format +msgid "all %s" +msgstr "toate %s" + +#: .\contrib\admin\views\doc.py:226 +#, python-format +msgid "number of %s" +msgstr "numarul de %s" + +#: .\contrib\admin\views\doc.py:231 +#, python-format +msgid "Fields on %s objects" +msgstr "Campuri in %s obiecte" + +#: .\contrib\admin\views\doc.py:293 +#: .\contrib\admin\views\doc.py:304 +#: .\contrib\admin\views\doc.py:306 +#: .\contrib\admin\views\doc.py:312 +#: .\contrib\admin\views\doc.py:313 +#: .\contrib\admin\views\doc.py:315 +msgid "Integer" +msgstr "Intreg" + +#: .\contrib\admin\views\doc.py:294 +msgid "Boolean (Either True or False)" +msgstr "Boolean (ori adevarat ori fals)" + +#: .\contrib\admin\views\doc.py:295 +#: .\contrib\admin\views\doc.py:314 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "Sir de caractere (pana la %(max_length)s caractere)" + +#: .\contrib\admin\views\doc.py:296 +msgid "Comma-separated integers" +msgstr "Numere intregi separate de virgule" + +#: .\contrib\admin\views\doc.py:297 +msgid "Date (without time)" +msgstr "Data (fara ora)" + +#: .\contrib\admin\views\doc.py:298 +msgid "Date (with time)" +msgstr "Data (cu ora)" + +#: .\contrib\admin\views\doc.py:299 +msgid "Decimal number" +msgstr "Numar zecimal" + +#: .\contrib\admin\views\doc.py:300 +msgid "E-mail address" +msgstr "Adresa email" + +#: .\contrib\admin\views\doc.py:301 +#: .\contrib\admin\views\doc.py:302 +#: .\contrib\admin\views\doc.py:305 +msgid "File path" +msgstr "Calea fisierului" + +#: .\contrib\admin\views\doc.py:303 +msgid "Floating point number" +msgstr "Numar cu virgula" + +#: .\contrib\admin\views\doc.py:307 +#: .\contrib\comments\models.py:89 +msgid "IP address" +msgstr "adresa ip" + +#: .\contrib\admin\views\doc.py:309 +msgid "Boolean (Either True, False or None)" +msgstr "Boolean (adevarat, fals sau nici una)" + +#: .\contrib\admin\views\doc.py:310 +msgid "Relation to parent model" +msgstr "Relatia cu un model parinte" + +#: .\contrib\admin\views\doc.py:311 +msgid "Phone number" +msgstr "Numar de telefon" + +#: .\contrib\admin\views\doc.py:316 +msgid "Text" +msgstr "Text" + +#: .\contrib\admin\views\doc.py:317 +msgid "Time" +msgstr "Timp" + +#: .\contrib\admin\views\doc.py:318 +#: .\contrib\flatpages\models.py:8 +msgid "URL" +msgstr "URL" + +#: .\contrib\admin\views\doc.py:319 +msgid "U.S. state (two uppercase letters)" +msgstr "Stat SUA (doua litere mari)" + +#: .\contrib\admin\views\doc.py:320 +msgid "XML text" +msgstr "Text XML" + +#: .\contrib\admin\views\doc.py:346 +#, python-format +msgid "%s does not appear to be a urlpattern object" +msgstr "%s nu pare a fi un obiect urlpattern" + +#: .\contrib\admin\views\main.py:233 +msgid "Site administration" +msgstr "Administrare site" + +#: .\contrib\admin\views\main.py:280 +#: .\contrib\admin\views\main.py:365 +#, python-format +msgid "You may add another %s below." +msgstr "Mai puteti adauga un alt %s mai jos." + +#: .\contrib\admin\views\main.py:298 +#, python-format +msgid "Add %s" +msgstr "Adaugă %s" + +#: .\contrib\admin\views\main.py:344 +#, python-format +msgid "Added %s." +msgstr "Adaugat %s." + +#: .\contrib\admin\views\main.py:344 +#: .\contrib\admin\views\main.py:346 +#: .\contrib\admin\views\main.py:348 +#: .\core\validators.py:283 +#: .\db\models\manipulators.py:309 +msgid "and" +msgstr "si" + +#: .\contrib\admin\views\main.py:346 +#, python-format +msgid "Changed %s." +msgstr "Schimbă %s." + +#: .\contrib\admin\views\main.py:348 +#, python-format +msgid "Deleted %s." +msgstr "Am sters %s." + +#: .\contrib\admin\views\main.py:351 +msgid "No fields changed." +msgstr "Nu s-a facut nicio schimbare." + +#: .\contrib\admin\views\main.py:354 +#, python-format +msgid "The %(name)s \"%(obj)s\" was changed successfully." +msgstr "%(name)s \"%(obj)s\" au fost modificate cu succes." + +#: .\contrib\admin\views\main.py:362 +#, python-format +msgid "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." +msgstr "%(name)s \"%(obj)s\" au fost adaugate cu succes. Le puteti edita mai jos." + +#: .\contrib\admin\views\main.py:400 +#, python-format +msgid "Change %s" +msgstr "Schimbă %s" + +#: .\contrib\admin\views\main.py:487 +#, python-format +msgid "One or more %(fieldname)s in %(name)s: %(obj)s" +msgstr "Unul sau mai multe %(fieldname)s in %(name)s: %(obj)s" + +#: .\contrib\admin\views\main.py:492 +#, python-format +msgid "One or more %(fieldname)s in %(name)s:" +msgstr "Unul sau mai multe %(fieldname)s in %(name)s:" + +#: .\contrib\admin\views\main.py:524 +#, python-format +msgid "The %(name)s \"%(obj)s\" was deleted successfully." +msgstr "%(name)s \"%(obj)s\" au fost sterse cu succes." + +#: .\contrib\admin\views\main.py:527 +msgid "Are you sure?" +msgstr "Sunteti sigur?" + +#: .\contrib\admin\views\main.py:549 +#, python-format +msgid "Change history: %s" +msgstr "Schimbari facute: %s" + +#: .\contrib\admin\views\main.py:583 +#, python-format +msgid "Select %s" +msgstr "Selecteaza %s" + +#: .\contrib\admin\views\main.py:583 +#, python-format +msgid "Select %s to change" +msgstr "Selecteaza %s pentru schimbare" + +#: .\contrib\admin\views\main.py:784 +msgid "Database error" +msgstr "Erroare de baza de date" + +#: .\contrib\auth\forms.py:17 +#: .\contrib\auth\forms.py:138 +msgid "The two password fields didn't match." +msgstr "Cele doua campuri pentru parole nu au coincis." + +#: .\contrib\auth\forms.py:25 +msgid "A user with that username already exists." +msgstr "Un utilizator cu acest nume de utilizator exista deja," + +#: .\contrib\auth\forms.py:53 +msgid "Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in." +msgstr "Se pare ca browserul dumneavoastra nu suporta cookies. Aveti nevoie de un browser care suporta cookies ca sa va logati." + +#: .\contrib\auth\forms.py:62 +msgid "This account is inactive." +msgstr "Acest cont este inactiv." + +#: .\contrib\auth\forms.py:84 +msgid "That e-mail address doesn't have an associated user account. Are you sure you've registered?" +msgstr "Acea adresa de e-mail nu are un nume de utilizator asociat. Sunteti sigur ca v-ati inregistrat?" + +#: .\contrib\auth\forms.py:107 +#, python-format +msgid "Password reset on %s" +msgstr "Parola resetata pe %s" + +#: .\contrib\auth\forms.py:117 +msgid "The two 'new password' fields didn't match." +msgstr "Cele doua campuri 'parola noua' nu au coincis." + +#: .\contrib\auth\forms.py:124 +msgid "Your old password was entered incorrectly. Please enter it again." +msgstr "Parola veche a dumneavoastra a fost introdusa incorect. Va rugam introduceti-o din nou." + +#: .\contrib\auth\models.py:73 +#: .\contrib\auth\models.py:93 msgid "name" msgstr "nume" -#: contrib/auth/models.py:15 +#: .\contrib\auth\models.py:75 msgid "codename" msgstr "nume cod" -#: contrib/auth/models.py:17 +#: .\contrib\auth\models.py:78 msgid "permission" msgstr "permisiune" -#: contrib/auth/models.py:18 contrib/auth/models.py:27 +#: .\contrib\auth\models.py:79 +#: .\contrib\auth\models.py:94 msgid "permissions" msgstr "permisiuni" -#: contrib/auth/models.py:29 +#: .\contrib\auth\models.py:97 msgid "group" msgstr "grup" -#: contrib/auth/models.py:30 contrib/auth/models.py:65 +#: .\contrib\auth\models.py:98 +#: .\contrib\auth\models.py:141 msgid "groups" msgstr "grupuri" -#: contrib/auth/models.py:55 +#: .\contrib\auth\models.py:131 msgid "username" msgstr "nume utilizator" -#: contrib/auth/models.py:56 +#: .\contrib\auth\models.py:131 +msgid "Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)." +msgstr "Necesar. 30 de caractere sau mai putin. Doar caractere alfanumerice (litere, cifre, liniute de subliniere)." + +#: .\contrib\auth\models.py:132 msgid "first name" msgstr "Prenume" -#: contrib/auth/models.py:57 +#: .\contrib\auth\models.py:133 msgid "last name" msgstr "Nume" -#: contrib/auth/models.py:58 +#: .\contrib\auth\models.py:134 msgid "e-mail address" msgstr "adresa email" -#: contrib/auth/models.py:59 +#: .\contrib\auth\models.py:135 msgid "password" msgstr "parola" -#: contrib/auth/models.py:59 -msgid "Use '[algo]$[salt]$[hexdigest]'" -msgstr "" +#: .\contrib\auth\models.py:135 +msgid "Use '[algo]$[salt]$[hexdigest]' or use the change password form." +msgstr "Folositi '[algo]$[salt]$[hexdigest]' sau folositi formularul de schimbare a parolei." -#: contrib/auth/models.py:60 +#: .\contrib\auth\models.py:136 msgid "staff status" msgstr "stare staff" -#: contrib/auth/models.py:60 +#: .\contrib\auth\models.py:136 msgid "Designates whether the user can log into this admin site." msgstr "Decide cînd utilizatorul se poate loga în acest sit de adminstrare." -#: contrib/auth/models.py:61 +#: .\contrib\auth\models.py:137 msgid "active" msgstr "activ" -#: contrib/auth/models.py:62 +#: .\contrib\auth\models.py:137 +msgid "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." +msgstr "Indica daca acest user va fi tratat ca activ sau nu. Deselecteaza-l in loc de a sterge conturi." + +#: .\contrib\auth\models.py:138 msgid "superuser status" msgstr "stare superutilizator" -#: contrib/auth/models.py:63 +#: .\contrib\auth\models.py:138 +msgid "Designates that this user has all permissions without explicitly assigning them." +msgstr "Indica daca acest user are toate permisiile fara a le selecta explicit." + +#: .\contrib\auth\models.py:139 msgid "last login" msgstr "ultima logare" -#: contrib/auth/models.py:64 +#: .\contrib\auth\models.py:140 msgid "date joined" msgstr "data aderării" -#: contrib/auth/models.py:66 -msgid "" -"In addition to the permissions manually assigned, this user will also get " -"all permissions granted to each group he/she is in." -msgstr "" -"Suplimentar permisiunilor manual alocate, acest utilizator va primi toate " -"permisiunile alocate fiecărui grup din care el/ea face parte." +#: .\contrib\auth\models.py:142 +msgid "In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in." +msgstr "Suplimentar permisiunilor manual alocate, acest utilizator va primi toate permisiunile alocate fiecărui grup din care el/ea face parte." -#: contrib/auth/models.py:67 +#: .\contrib\auth\models.py:143 msgid "user permissions" msgstr "permisiuni utilizator" -#: contrib/auth/models.py:70 +#: .\contrib\auth\models.py:147 msgid "user" msgstr "utilizator" -#: contrib/auth/models.py:71 +#: .\contrib\auth\models.py:148 msgid "users" msgstr "utilizatori" -#: contrib/auth/models.py:76 +#: .\contrib\auth\models.py:154 msgid "Personal info" msgstr "Informaţii personale" -#: contrib/auth/models.py:77 +#: .\contrib\auth\models.py:155 msgid "Permissions" msgstr "Permisiuni" -#: contrib/auth/models.py:78 +#: .\contrib\auth\models.py:156 msgid "Important dates" msgstr "Date importante" -#: contrib/auth/models.py:79 +#: .\contrib\auth\models.py:157 msgid "Groups" msgstr "Grupuri" -#: contrib/auth/models.py:219 +#: .\contrib\auth\models.py:316 msgid "message" msgstr "mesaj" -#: contrib/auth/forms.py:30 -msgid "" -"Your Web browser doesn't appear to have cookies enabled. Cookies are " -"required for logging in." -msgstr "" -"Se pare ca browserul dumneavoastra nu suporta cookies. Aveti nevoie de un " -"browser care suporta cookies ca sa va logati." +#: .\contrib\auth\views.py:47 +msgid "Logged out" +msgstr "Deautentificat" -#: contrib/contenttypes/models.py:25 +#: .\contrib\comments\models.py:71 +#: .\contrib\comments\models.py:176 +msgid "object ID" +msgstr "id obiect" + +#: .\contrib\comments\models.py:72 +msgid "headline" +msgstr "titlu" + +#: .\contrib\comments\models.py:73 +#: .\contrib\comments\models.py:95 +#: .\contrib\comments\models.py:177 +msgid "comment" +msgstr "comentariu" + +#: .\contrib\comments\models.py:74 +msgid "rating #1" +msgstr "rating #1" + +#: .\contrib\comments\models.py:75 +msgid "rating #2" +msgstr "rating #2" + +#: .\contrib\comments\models.py:76 +msgid "rating #3" +msgstr "rating #3" + +#: .\contrib\comments\models.py:77 +msgid "rating #4" +msgstr "rating #4" + +#: .\contrib\comments\models.py:78 +msgid "rating #5" +msgstr "rating #5" + +#: .\contrib\comments\models.py:79 +msgid "rating #6" +msgstr "rating #6" + +#: .\contrib\comments\models.py:80 +msgid "rating #7" +msgstr "rating #7" + +#: .\contrib\comments\models.py:81 +msgid "rating #8" +msgstr "rating #8" + +#: .\contrib\comments\models.py:86 +msgid "is valid rating" +msgstr "este rating valid" + +#: .\contrib\comments\models.py:87 +#: .\contrib\comments\models.py:179 +msgid "date/time submitted" +msgstr "data/ora crearii" + +#: .\contrib\comments\models.py:88 +#: .\contrib\comments\models.py:180 +msgid "is public" +msgstr "public" + +#: .\contrib\comments\models.py:90 +msgid "is removed" +msgstr "sters" + +#: .\contrib\comments\models.py:90 +msgid "Check this box if the comment is inappropriate. A \"This comment has been removed\" message will be displayed instead." +msgstr "Bifeaza aceasta casuta daca comentariul nu este adecvat. Un mesaj de tipul \"Acest comentariu a fost sters\" va fi afisat in schimb." + +#: .\contrib\comments\models.py:96 +msgid "comments" +msgstr "comentarii" + +#: .\contrib\comments\models.py:140 +#: .\contrib\comments\models.py:222 +msgid "Content object" +msgstr "Obiect-referinta" + +#: .\contrib\comments\models.py:168 +#, python-format +msgid "" +"Posted by %(user)s at %(date)s\n" +"\n" +"%(comment)s\n" +"\n" +"http://%(domain)s%(url)s" +msgstr "" +"Postat de %(user)s la %(date)s\n" +"\n" +"%(comment)s\n" +"\n" +"http://%(domain)s%(url)s" + +#: .\contrib\comments\models.py:178 +msgid "person's name" +msgstr "numele persoanei" + +#: .\contrib\comments\models.py:181 +msgid "ip address" +msgstr "adresa ip" + +#: .\contrib\comments\models.py:183 +msgid "approved by staff" +msgstr "aprobat de echipa" + +#: .\contrib\comments\models.py:187 +msgid "free comment" +msgstr "permite comentariu fara inregistrare" + +#: .\contrib\comments\models.py:188 +msgid "free comments" +msgstr "permite comentarii fara inregistrare" + +#: .\contrib\comments\models.py:250 +msgid "score" +msgstr "punctaj" + +#: .\contrib\comments\models.py:251 +msgid "score date" +msgstr "data punctajului" + +#: .\contrib\comments\models.py:255 +#, fuzzy +msgid "karma score" +msgstr "punctaj karmic" + +#: .\contrib\comments\models.py:256 +#, fuzzy +msgid "karma scores" +msgstr "punctaje karmice" + +#: .\contrib\comments\models.py:260 +#, fuzzy, python-format +msgid "%(score)d rating by %(user)s" +msgstr "Punctaj %(score)d atribuit de catre %(user)s" + +#: .\contrib\comments\models.py:277 +#, python-format +msgid "" +"This comment was flagged by %(user)s:\n" +"\n" +"%(text)s" +msgstr "" +"Acest comentariu a fost marcat de %(user)s:\n" +"\n" +"%(text)s" + +#: .\contrib\comments\models.py:285 +msgid "flag date" +msgstr "data marcarii" + +#: .\contrib\comments\models.py:289 +#, fuzzy +msgid "user flag" +msgstr "stegulet de utilizator" + +#: .\contrib\comments\models.py:290 +#, fuzzy +msgid "user flags" +msgstr "stegulete de utilizatori" + +#: .\contrib\comments\models.py:294 +#, python-format +msgid "Flag by %r" +msgstr "Marcajul lui %r" + +#: .\contrib\comments\models.py:300 +msgid "deletion date" +msgstr "data stergerii" + +#: .\contrib\comments\models.py:303 +msgid "moderator deletion" +msgstr "sters de moderator" + +#: .\contrib\comments\models.py:304 +msgid "moderator deletions" +msgstr "sterse de moderator" + +#: .\contrib\comments\models.py:308 +#, python-format +msgid "Moderator deletion by %r" +msgstr "Sters de moderatorul %r" + +#: .\contrib\comments\templates\comments\form.html.py:8 +msgid "Forgotten your password?" +msgstr "Ai uitat parola?" + +#: .\contrib\comments\templates\comments\form.html.py:12 +msgid "Ratings" +msgstr "Ratinguri" + +#: .\contrib\comments\templates\comments\form.html.py:12 +#: .\contrib\comments\templates\comments\form.html.py:23 +msgid "Required" +msgstr "Necesar" + +#: .\contrib\comments\templates\comments\form.html.py:12 +#: .\contrib\comments\templates\comments\form.html.py:23 +msgid "Optional" +msgstr "Optional" + +#: .\contrib\comments\templates\comments\form.html.py:23 +msgid "Post a photo" +msgstr "Adauga o poza" + +#: .\contrib\comments\templates\comments\form.html.py:28 +#: .\contrib\comments\templates\comments\freeform.html.py:5 +msgid "Comment:" +msgstr "Comentariu:" + +#: .\contrib\comments\templates\comments\form.html.py:35 +#: .\contrib\comments\templates\comments\freeform.html.py:10 +msgid "Preview comment" +msgstr "Previzualizare comentariu" + +#: .\contrib\comments\templates\comments\freeform.html.py:4 +msgid "Your name:" +msgstr "numele dumneavoastra" + +#: .\contrib\comments\views\comments.py:28 +msgid "This rating is required because you've entered at least one other rating." +msgstr "Acest rating este necesar deoarece ai introdus cel putin un alt rating." + +#: .\contrib\comments\views\comments.py:112 +#, python-format +msgid "" +"This comment was posted by a user who has posted fewer than %(count)s comment:\n" +"\n" +"%(text)s" +msgid_plural "" +"This comment was posted by a user who has posted fewer than %(count)s comments:\n" +"\n" +"%(text)s" +msgstr[0] "" +"Acest comentariu a fost postat de un utilizator care a postat mai putin de %(count)s comentariu:\n" +"\n" +"%(text)s" +msgstr[1] "" +"Acest comentariu a fost postat de un utilizator care a postat mai putin de %(count)s comentarii:\n" +"\n" +"%(text)s" + +#: .\contrib\comments\views\comments.py:117 +#, python-format +msgid "" +"This comment was posted by a sketchy user:\n" +"\n" +"%(text)s" +msgstr "" +"Acest comentariu a fost postat de un utilizator neordonat:\n" +"\n" +"%(text)s" + +#: .\contrib\comments\views\comments.py:190 +#: .\contrib\comments\views\comments.py:283 +msgid "Only POSTs are allowed" +msgstr "Doar postarile sunt permise" + +#: .\contrib\comments\views\comments.py:194 +#: .\contrib\comments\views\comments.py:287 +msgid "One or more of the required fields wasn't submitted" +msgstr "Unul sau mai multe campuri necesare nu a fost completat" + +#: .\contrib\comments\views\comments.py:198 +#: .\contrib\comments\views\comments.py:289 +msgid "Somebody tampered with the comment form (security violation)" +msgstr "Cineva a modificat formularul de comentarii (incalcare a securitatii)" + +#: .\contrib\comments\views\comments.py:208 +#: .\contrib\comments\views\comments.py:295 +msgid "The comment form had an invalid 'target' parameter -- the object ID was invalid" +msgstr "Formularul de comentariu a avut un parametru 'tinta' invalid -- ID-ul obiectului a fost invalid" + +#: .\contrib\comments\views\comments.py:259 +#: .\contrib\comments\views\comments.py:324 +msgid "The comment form didn't provide either 'preview' or 'post'" +msgstr "Formularul de comentariu nu specifica nici 'previzualizare' nici 'postare'" + +#: .\contrib\comments\views\karma.py:21 +msgid "Anonymous users cannot vote" +msgstr "Utilizatorii anonimi nu pot vota" + +#: .\contrib\comments\views\karma.py:25 +msgid "Invalid comment ID" +msgstr "Comentariu cu ID invalid" + +#: .\contrib\comments\views\karma.py:27 +msgid "No voting for yourself" +msgstr "Nu poti vota pentru tine" + +#: .\contrib\contenttypes\models.py:67 #, fuzzy msgid "python model class name" -msgstr "nume modul python" +msgstr "nume clasa model python" -#: contrib/contenttypes/models.py:28 +#: .\contrib\contenttypes\models.py:71 msgid "content type" msgstr "tip conţinut" -#: contrib/contenttypes/models.py:29 +#: .\contrib\contenttypes\models.py:72 msgid "content types" msgstr "tipuri conţinute" -#: contrib/sessions/models.py:35 +#: .\contrib\flatpages\models.py:9 +msgid "Example: '/about/contact/'. Make sure to have leading and trailing slashes." +msgstr "Exemplu: '/about/contact'. Asiguraţi-vă că aveţi slash-uri la început şi la sfîrşit." + +#: .\contrib\flatpages\models.py:10 +msgid "title" +msgstr "titlu" + +#: .\contrib\flatpages\models.py:11 +msgid "content" +msgstr "conţinut" + +#: .\contrib\flatpages\models.py:12 +msgid "enable comments" +msgstr "permite comentarii" + +#: .\contrib\flatpages\models.py:13 +msgid "template name" +msgstr "nume şablon" + +#: .\contrib\flatpages\models.py:14 +msgid "Example: 'flatpages/contact_page.html'. If this isn't provided, the system will use 'flatpages/default.html'." +msgstr "Exemplu: 'flatpages/contact_page.html'. Dacă aceasta nu există, sistemul va folosi 'flatpages/default.html'." + +#: .\contrib\flatpages\models.py:15 +msgid "registration required" +msgstr "necesită înregistrare" + +#: .\contrib\flatpages\models.py:15 +msgid "If this is checked, only logged-in users will be able to view the page." +msgstr "Dacă aceasta este bifată, numai utilizatorii logaţi vor putea vedea pagina." + +#: .\contrib\flatpages\models.py:20 +msgid "flat page" +msgstr "pagina plată" + +#: .\contrib\flatpages\models.py:21 +msgid "flat pages" +msgstr "pagini plate" + +#: .\contrib\flatpages\models.py:27 +msgid "Advanced options" +msgstr "Optiuni avansate" + +#: .\contrib\humanize\templatetags\humanize.py:19 +msgid "th" +msgstr "ul" + +#: .\contrib\humanize\templatetags\humanize.py:19 +msgid "st" +msgstr "ul" + +#: .\contrib\humanize\templatetags\humanize.py:19 +msgid "nd" +msgstr "lea" + +#: .\contrib\humanize\templatetags\humanize.py:19 +msgid "rd" +msgstr "lea" + +#: .\contrib\humanize\templatetags\humanize.py:51 +#, python-format +msgid "%(value).1f million" +msgid_plural "%(value).1f million" +msgstr[0] "%(value).1f milion" +msgstr[1] "%(value).1f milioane" + +#: .\contrib\humanize\templatetags\humanize.py:54 +#, python-format +msgid "%(value).1f billion" +msgid_plural "%(value).1f billion" +msgstr[0] "%(value).1f miliard" +msgstr[1] "%(value).1f miliarde" + +#: .\contrib\humanize\templatetags\humanize.py:57 +#, python-format +msgid "%(value).1f trillion" +msgid_plural "%(value).1f trillion" +msgstr[0] "%(value).1f mie de miliarde" +msgstr[1] "%(value).1f mii de miliarde" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "one" +msgstr "unu" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "two" +msgstr "doi" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "three" +msgstr "trei" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "four" +msgstr "patry" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "five" +msgstr "cinci" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "six" +msgstr "sase" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "seven" +msgstr "sapte" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "eight" +msgstr "opt" + +#: .\contrib\humanize\templatetags\humanize.py:73 +msgid "nine" +msgstr "noua" + +#: .\contrib\humanize\templatetags\humanize.py:93 +msgid "today" +msgstr "astazi" + +#: .\contrib\humanize\templatetags\humanize.py:95 +msgid "tomorrow" +msgstr "maine" + +#: .\contrib\humanize\templatetags\humanize.py:97 +msgid "yesterday" +msgstr "ieri" + +#: .\contrib\localflavor\ar\forms.py:27 +msgid "Enter a postal code in the format NNNN or ANNNNAAA." +msgstr "Introduceti un cod postal valid de forma NNNN sau ANNNNAAA." + +#: .\contrib\localflavor\ar\forms.py:49 +#: .\contrib\localflavor\br\forms.py:96 +#: .\contrib\localflavor\br\forms.py:135 +#: .\contrib\localflavor\pe\forms.py:23 +#: .\contrib\localflavor\pe\forms.py:51 +msgid "This field requires only numbers." +msgstr "Acest camp accepta doar numere." + +#: .\contrib\localflavor\ar\forms.py:50 +msgid "This field requires 7 or 8 digits." +msgstr "Acest camp are nevoie de 7 sau 8 cifre." + +#: .\contrib\localflavor\ar\forms.py:79 +msgid "Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format." +msgstr "Introduceti un CUIT valid de forma XX-XXXXXXXX-X sau XXXXXXXXXXXX." + +#: .\contrib\localflavor\ar\forms.py:80 +msgid "Invalid CUIT." +msgstr "CUIT invalid." + +#: .\contrib\localflavor\au\forms.py:16 +msgid "Enter a 4 digit post code." +msgstr "Introduceti un cod postal de 4 cifre." + +#: .\contrib\localflavor\br\forms.py:21 +msgid "Enter a zip code in the format XXXXX-XXX." +msgstr "Adaugati un cod postal de forma XXXXX-XXX." + +#: .\contrib\localflavor\br\forms.py:30 +msgid "Phone numbers must be in XX-XXXX-XXXX format." +msgstr "Numerele de telefon trebuie să fie in format XXX-XXX-XXXX." + +#: .\contrib\localflavor\br\forms.py:58 +msgid "Select a valid brazilian state. That state is not one of the available states." +msgstr "Selectati un stat brazilian valid. Acest stat nu se afla printre statele disponibile." + +#: .\contrib\localflavor\br\forms.py:94 +msgid "Invalid CPF number." +msgstr "Numar CPF invalid." + +#: .\contrib\localflavor\br\forms.py:95 +msgid "This field requires at most 11 digits or 14 characters." +msgstr "Acest camp are nevoie de cel mult 11 cifre sau 14 caractere." + +#: .\contrib\localflavor\br\forms.py:134 +msgid "Invalid CNPJ number." +msgstr "Numar CNPJ invalid." + +#: .\contrib\localflavor\br\forms.py:136 +msgid "This field requires at least 14 digits" +msgstr "Acest camd are nevoie de cel putin 14 cifre." + +#: .\contrib\localflavor\ca\forms.py:17 +msgid "Enter a postal code in the format XXX XXX." +msgstr "Introduceti un cod postal de forma XXX XXX." + +#: .\contrib\localflavor\ca\forms.py:88 +msgid "Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format." +msgstr "Introduceti un numar canadia de asigurare sociala valid de forma XXX-XXX-XXX." + +#: .\contrib\localflavor\ch\ch_states.py:5 +msgid "Aargau" +msgstr "Aargau" + +#: .\contrib\localflavor\ch\ch_states.py:6 +msgid "Appenzell Innerrhoden" +msgstr "Appenzell Innerrhoden" + +#: .\contrib\localflavor\ch\ch_states.py:7 +msgid "Appenzell Ausserrhoden" +msgstr "Appenzell Ausserrhoden" + +#: .\contrib\localflavor\ch\ch_states.py:8 +msgid "Basel-Stadt" +msgstr "Basel-Stadt" + +#: .\contrib\localflavor\ch\ch_states.py:9 +msgid "Basel-Land" +msgstr "Basel-Land" + +#: .\contrib\localflavor\ch\ch_states.py:10 +msgid "Berne" +msgstr "Berna" + +#: .\contrib\localflavor\ch\ch_states.py:11 +msgid "Fribourg" +msgstr "Fribourg" + +#: .\contrib\localflavor\ch\ch_states.py:12 +msgid "Geneva" +msgstr "Geneva" + +#: .\contrib\localflavor\ch\ch_states.py:13 +msgid "Glarus" +msgstr "Glarus" + +#: .\contrib\localflavor\ch\ch_states.py:14 +msgid "Graubuenden" +msgstr "Graubuenden" + +#: .\contrib\localflavor\ch\ch_states.py:15 +msgid "Jura" +msgstr "Jura" + +#: .\contrib\localflavor\ch\ch_states.py:16 +msgid "Lucerne" +msgstr "Lucerna" + +#: .\contrib\localflavor\ch\ch_states.py:17 +msgid "Neuchatel" +msgstr "Neuchatel" + +#: .\contrib\localflavor\ch\ch_states.py:18 +msgid "Nidwalden" +msgstr "Nidwalden" + +#: .\contrib\localflavor\ch\ch_states.py:19 +msgid "Obwalden" +msgstr "Obwalden" + +#: .\contrib\localflavor\ch\ch_states.py:20 +msgid "Schaffhausen" +msgstr "Schaffhausen" + +#: .\contrib\localflavor\ch\ch_states.py:21 +msgid "Schwyz" +msgstr "Schwyz" + +#: .\contrib\localflavor\ch\ch_states.py:22 +msgid "Solothurn" +msgstr "Solothurn" + +#: .\contrib\localflavor\ch\ch_states.py:23 +msgid "St. Gallen" +msgstr "St. Gallen" + +#: .\contrib\localflavor\ch\ch_states.py:24 +msgid "Thurgau" +msgstr "JoiTurgovia" + +#: .\contrib\localflavor\ch\ch_states.py:25 +msgid "Ticino" +msgstr "Ticino" + +#: .\contrib\localflavor\ch\ch_states.py:26 +msgid "Uri" +msgstr "Uri" + +#: .\contrib\localflavor\ch\ch_states.py:27 +msgid "Valais" +msgstr "Valais" + +#: .\contrib\localflavor\ch\ch_states.py:28 +msgid "Vaud" +msgstr "Vaud" + +#: .\contrib\localflavor\ch\ch_states.py:29 +msgid "Zug" +msgstr "Zug" + +#: .\contrib\localflavor\ch\ch_states.py:30 +msgid "Zurich" +msgstr "Zurich" + +#: .\contrib\localflavor\ch\forms.py:16 +#: .\contrib\localflavor\no\forms.py:12 +msgid "Enter a zip code in the format XXXX." +msgstr "Introduceti un cod postal de forma XXXX." + +#: .\contrib\localflavor\ch\forms.py:64 +msgid "Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format." +msgstr "Introduceti un numar elvetian de identitate sau pasaport, valid, de forma X1234567<0 sau 1234567890." + +#: .\contrib\localflavor\cl\forms.py:29 +msgid "Enter a valid Chilean RUT." +msgstr "Introduceti RUT chilian valid." + +#: .\contrib\localflavor\cl\forms.py:30 +msgid "Enter a valid Chilean RUT. The format is XX.XXX.XXX-X." +msgstr "Introduceti un RUT chilian valid. Formatul este XX.XXX.XXX-X." + +#: .\contrib\localflavor\cl\forms.py:31 +msgid "The Chilean RUT is not valid." +msgstr "RUT-ul chilean nu este valid." + +#: .\contrib\localflavor\de\de_states.py:5 +msgid "Baden-Wuerttemberg" +msgstr "Baden-Wuerttemberg" + +#: .\contrib\localflavor\de\de_states.py:6 +msgid "Bavaria" +msgstr "Bavaria" + +#: .\contrib\localflavor\de\de_states.py:7 +msgid "Berlin" +msgstr "Berlin" + +#: .\contrib\localflavor\de\de_states.py:8 +msgid "Brandenburg" +msgstr "Brandenburg" + +#: .\contrib\localflavor\de\de_states.py:9 +msgid "Bremen" +msgstr "Bremen" + +#: .\contrib\localflavor\de\de_states.py:10 +msgid "Hamburg" +msgstr "Hamburg" + +#: .\contrib\localflavor\de\de_states.py:11 +msgid "Hessen" +msgstr "Hessa" + +#: .\contrib\localflavor\de\de_states.py:12 +msgid "Mecklenburg-Western Pomerania" +msgstr "Mecklenburg-Pomerania Inferioara" + +#: .\contrib\localflavor\de\de_states.py:13 +msgid "Lower Saxony" +msgstr "Saxonia Inferioara" + +#: .\contrib\localflavor\de\de_states.py:14 +msgid "North Rhine-Westphalia" +msgstr "Renania de Nord-Westfalia" + +#: .\contrib\localflavor\de\de_states.py:15 +msgid "Rhineland-Palatinate" +msgstr "Renanina-Palatinat" + +#: .\contrib\localflavor\de\de_states.py:16 +msgid "Saarland" +msgstr "Saarland" + +#: .\contrib\localflavor\de\de_states.py:17 +msgid "Saxony" +msgstr "Saxonia" + +#: .\contrib\localflavor\de\de_states.py:18 +msgid "Saxony-Anhalt" +msgstr "Saxonia-Anhalt" + +#: .\contrib\localflavor\de\de_states.py:19 +msgid "Schleswig-Holstein" +msgstr "Schleswig-Holstein" + +#: .\contrib\localflavor\de\de_states.py:20 +msgid "Thuringia" +msgstr "Turingia" + +#: .\contrib\localflavor\de\forms.py:14 +#: .\contrib\localflavor\fi\forms.py:12 +#: .\contrib\localflavor\fr\forms.py:15 +msgid "Enter a zip code in the format XXXXX." +msgstr "Introduceti un cod postal de forma XXXXX." + +#: .\contrib\localflavor\de\forms.py:41 +msgid "Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X format." +msgstr "Introduceti un numar german de identitate valid, de forma XXXXXXXXXXX-XXXXXXX-XXXXXXX-X." + +#: .\contrib\localflavor\es\es_provinces.py:5 +msgid "Arava" +msgstr "Arava" + +#: .\contrib\localflavor\es\es_provinces.py:6 +msgid "Albacete" +msgstr "Albacete" + +#: .\contrib\localflavor\es\es_provinces.py:7 +msgid "Alacant" +msgstr "Alacant" + +#: .\contrib\localflavor\es\es_provinces.py:8 +msgid "Almeria" +msgstr "Almeria" + +#: .\contrib\localflavor\es\es_provinces.py:9 +msgid "Avila" +msgstr "Avila" + +#: .\contrib\localflavor\es\es_provinces.py:10 +msgid "Badajoz" +msgstr "Badajoz" + +#: .\contrib\localflavor\es\es_provinces.py:11 +msgid "Illes Balears" +msgstr "Insulele Baleare" + +#: .\contrib\localflavor\es\es_provinces.py:12 +msgid "Barcelona" +msgstr "Barcelona" + +#: .\contrib\localflavor\es\es_provinces.py:13 +msgid "Burgos" +msgstr "Burgos" + +#: .\contrib\localflavor\es\es_provinces.py:14 +msgid "Caceres" +msgstr "Caceres" + +#: .\contrib\localflavor\es\es_provinces.py:15 +msgid "Cadiz" +msgstr "Cadiz" + +#: .\contrib\localflavor\es\es_provinces.py:16 +msgid "Castello" +msgstr "Castello" + +#: .\contrib\localflavor\es\es_provinces.py:17 +msgid "Ciudad Real" +msgstr "Ciudad Real" + +#: .\contrib\localflavor\es\es_provinces.py:18 +msgid "Cordoba" +msgstr "Cordoba" + +#: .\contrib\localflavor\es\es_provinces.py:19 +msgid "A Coruna" +msgstr "A Coruna" + +#: .\contrib\localflavor\es\es_provinces.py:20 +msgid "Cuenca" +msgstr "Cuenca" + +#: .\contrib\localflavor\es\es_provinces.py:21 +msgid "Girona" +msgstr "Girona" + +#: .\contrib\localflavor\es\es_provinces.py:22 +msgid "Granada" +msgstr "Granada" + +#: .\contrib\localflavor\es\es_provinces.py:23 +msgid "Guadalajara" +msgstr "Guadalajara" + +#: .\contrib\localflavor\es\es_provinces.py:24 +msgid "Guipuzkoa" +msgstr "Guipuzkoa" + +#: .\contrib\localflavor\es\es_provinces.py:25 +msgid "Huelva" +msgstr "Huelva" + +#: .\contrib\localflavor\es\es_provinces.py:26 +msgid "Huesca" +msgstr "Huesca" + +#: .\contrib\localflavor\es\es_provinces.py:27 +msgid "Jaen" +msgstr "Jaen" + +#: .\contrib\localflavor\es\es_provinces.py:28 +msgid "Leon" +msgstr "Leon" + +#: .\contrib\localflavor\es\es_provinces.py:29 +msgid "Lleida" +msgstr "Lleida" + +#: .\contrib\localflavor\es\es_provinces.py:30 +#: .\contrib\localflavor\es\es_regions.py:17 +msgid "La Rioja" +msgstr "La Rioja" + +#: .\contrib\localflavor\es\es_provinces.py:31 +msgid "Lugo" +msgstr "Lugo" + +#: .\contrib\localflavor\es\es_provinces.py:32 +#: .\contrib\localflavor\es\es_regions.py:18 +msgid "Madrid" +msgstr "Madrid" + +#: .\contrib\localflavor\es\es_provinces.py:33 +msgid "Malaga" +msgstr "Malaga" + +#: .\contrib\localflavor\es\es_provinces.py:34 +msgid "Murcia" +msgstr "Murcia" + +#: .\contrib\localflavor\es\es_provinces.py:35 +msgid "Navarre" +msgstr "Navarra" + +#: .\contrib\localflavor\es\es_provinces.py:36 +msgid "Ourense" +msgstr "Ourense" + +#: .\contrib\localflavor\es\es_provinces.py:37 +msgid "Asturias" +msgstr "Asturias" + +#: .\contrib\localflavor\es\es_provinces.py:38 +msgid "Palencia" +msgstr "Palencia" + +#: .\contrib\localflavor\es\es_provinces.py:39 +msgid "Las Palmas" +msgstr "Las Palmas" + +#: .\contrib\localflavor\es\es_provinces.py:40 +msgid "Pontevedra" +msgstr "Pontevedra" + +#: .\contrib\localflavor\es\es_provinces.py:41 +msgid "Salamanca" +msgstr "Salamanca" + +#: .\contrib\localflavor\es\es_provinces.py:42 +msgid "Santa Cruz de Tenerife" +msgstr "Santa Cruz de Tenerife" + +#: .\contrib\localflavor\es\es_provinces.py:43 +#: .\contrib\localflavor\es\es_regions.py:11 +msgid "Cantabria" +msgstr "Cantabria" + +#: .\contrib\localflavor\es\es_provinces.py:44 +msgid "Segovia" +msgstr "Segovia" + +#: .\contrib\localflavor\es\es_provinces.py:45 +msgid "Seville" +msgstr "Sevilia" + +#: .\contrib\localflavor\es\es_provinces.py:46 +msgid "Soria" +msgstr "Soria" + +#: .\contrib\localflavor\es\es_provinces.py:47 +msgid "Tarragona" +msgstr "Tarragona" + +#: .\contrib\localflavor\es\es_provinces.py:48 +msgid "Teruel" +msgstr "Teruel" + +#: .\contrib\localflavor\es\es_provinces.py:49 +msgid "Toledo" +msgstr "Toledo" + +#: .\contrib\localflavor\es\es_provinces.py:50 +msgid "Valencia" +msgstr "Valencia" + +#: .\contrib\localflavor\es\es_provinces.py:51 +msgid "Valladolid" +msgstr "Valladolid" + +#: .\contrib\localflavor\es\es_provinces.py:52 +msgid "Bizkaia" +msgstr "Bizkaia" + +#: .\contrib\localflavor\es\es_provinces.py:53 +msgid "Zamora" +msgstr "Zamora" + +#: .\contrib\localflavor\es\es_provinces.py:54 +msgid "Zaragoza" +msgstr "Zaragoza" + +#: .\contrib\localflavor\es\es_provinces.py:55 +msgid "Ceuta" +msgstr "Ceuta" + +#: .\contrib\localflavor\es\es_provinces.py:56 +msgid "Melilla" +msgstr "Melilla" + +#: .\contrib\localflavor\es\es_regions.py:5 +msgid "Andalusia" +msgstr "Andalusia" + +#: .\contrib\localflavor\es\es_regions.py:6 +msgid "Aragon" +msgstr "Aragon" + +#: .\contrib\localflavor\es\es_regions.py:7 +msgid "Principality of Asturias" +msgstr "Principatul Asturiei" + +#: .\contrib\localflavor\es\es_regions.py:8 +msgid "Balearic Islands" +msgstr "Insulele Baleare" + +#: .\contrib\localflavor\es\es_regions.py:9 +msgid "Basque Country" +msgstr "Tara Bascilor" + +#: .\contrib\localflavor\es\es_regions.py:10 +msgid "Canary Islands" +msgstr "Insulele Canare" + +#: .\contrib\localflavor\es\es_regions.py:12 +msgid "Castile-La Mancha" +msgstr "Castile-La Mancha" + +#: .\contrib\localflavor\es\es_regions.py:13 +msgid "Castile and Leon" +msgstr "Castilia si Leon" + +#: .\contrib\localflavor\es\es_regions.py:14 +msgid "Catalonia" +msgstr "Catalonia" + +#: .\contrib\localflavor\es\es_regions.py:15 +msgid "Extremadura" +msgstr "Extremadura" + +#: .\contrib\localflavor\es\es_regions.py:16 +msgid "Galicia" +msgstr "Galicia" + +#: .\contrib\localflavor\es\es_regions.py:19 +msgid "Region of Murcia" +msgstr "Regiunea Murcia" + +#: .\contrib\localflavor\es\es_regions.py:20 +msgid "Foral Community of Navarre" +msgstr "Comunitatea Forala Navara" + +#: .\contrib\localflavor\es\es_regions.py:21 +msgid "Valencian Community" +msgstr "Comunitatea Valencia" + +#: .\contrib\localflavor\es\forms.py:19 +msgid "Enter a valid postal code in the range and format 01XXX - 52XXX." +msgstr "Introduceti un cod postal valid in intervalul si de forma 01XXX - 52XXX." + +#: .\contrib\localflavor\es\forms.py:39 +msgid "Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX." +msgstr "Introduceti un numa de telefon valid intr-unul dintre formatele 6XXXXXXXX, 8XXXXXXXX sau 9XXXXXXXX." + +#: .\contrib\localflavor\es\forms.py:66 +msgid "Please enter a valid NIF, NIE, or CIF." +msgstr "Introduceţi vă rog un NIF, NIE sau CIF valid." + +#: .\contrib\localflavor\es\forms.py:67 +msgid "Please enter a valid NIF or NIE." +msgstr "Introduceti va rog un NIF sau NIE valid." + +#: .\contrib\localflavor\es\forms.py:68 +msgid "Invalid checksum for NIF." +msgstr "Suma de control invalda pentru NIF." + +#: .\contrib\localflavor\es\forms.py:69 +msgid "Invalid checksum for NIE." +msgstr "Suma de control invalida pentru NIE." + +#: .\contrib\localflavor\es\forms.py:70 +msgid "Invalid checksum for CIF." +msgstr "Suma de control invalida pentru CIF." + +#: .\contrib\localflavor\es\forms.py:142 +msgid "Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX." +msgstr "Introduceti un numar de cont bancar valid de forma XXXX-XXXX-XX-XXXXXXXXXX." + +#: .\contrib\localflavor\es\forms.py:143 +msgid "Invalid checksum for bank account number." +msgstr "Suma de control invalida pentru numarul de cont bancar." + +#: .\contrib\localflavor\fi\forms.py:28 +msgid "Enter a valid Finnish social security number." +msgstr "Introduceti un numar finlandez de securitate sociala valid." + +#: .\contrib\localflavor\in_\forms.py:14 +msgid "Enter a zip code in the format XXXXXXX." +msgstr "Introduceti un cod postal de forma XXXXXXX." + +#: .\contrib\localflavor\is_\forms.py:17 +msgid "Enter a valid Icelandic identification number. The format is XXXXXX-XXXX." +msgstr "Introduceti un numa islandez de autentificare valid. Formatul este XXXXXX-XXXX." + +#: .\contrib\localflavor\is_\forms.py:18 +msgid "The Icelandic identification number is not valid." +msgstr "Numarul islandez de identificare nu este valid." + +#: .\contrib\localflavor\it\forms.py:14 +msgid "Enter a valid zip code." +msgstr "Introduceti un cod postal valid." + +#: .\contrib\localflavor\it\forms.py:43 +msgid "Enter a valid Social Security number." +msgstr "Introduceti un numar de securitate sociala valid." + +#: .\contrib\localflavor\it\forms.py:68 +msgid "Enter a valid VAT number." +msgstr "Introduceti un numar VAT valid." + +#: .\contrib\localflavor\jp\forms.py:17 +msgid "Enter a postal code in the format XXXXXXX or XXX-XXXX." +msgstr "Introduceti un cod postal de forma XXXXXXX sau XXX-XXXX." + +#: .\contrib\localflavor\jp\jp_prefectures.py:4 +msgid "Hokkaido" +msgstr "Hokkaido" + +#: .\contrib\localflavor\jp\jp_prefectures.py:5 +msgid "Aomori" +msgstr "Aomori" + +#: .\contrib\localflavor\jp\jp_prefectures.py:6 +msgid "Iwate" +msgstr "Iwate" + +#: .\contrib\localflavor\jp\jp_prefectures.py:7 +msgid "Miyagi" +msgstr "Miyagi" + +#: .\contrib\localflavor\jp\jp_prefectures.py:8 +msgid "Akita" +msgstr "Akita" + +#: .\contrib\localflavor\jp\jp_prefectures.py:9 +msgid "Yamagata" +msgstr "Yamagata" + +#: .\contrib\localflavor\jp\jp_prefectures.py:10 +msgid "Fukushima" +msgstr "Fukushima" + +#: .\contrib\localflavor\jp\jp_prefectures.py:11 +msgid "Ibaraki" +msgstr "Ibaraki" + +#: .\contrib\localflavor\jp\jp_prefectures.py:12 +msgid "Tochigi" +msgstr "Tochigi" + +#: .\contrib\localflavor\jp\jp_prefectures.py:13 +msgid "Gunma" +msgstr "Gunma" + +#: .\contrib\localflavor\jp\jp_prefectures.py:14 +msgid "Saitama" +msgstr "Saitama" + +#: .\contrib\localflavor\jp\jp_prefectures.py:15 +msgid "Chiba" +msgstr "Chiba" + +#: .\contrib\localflavor\jp\jp_prefectures.py:16 +msgid "Tokyo" +msgstr "Tokyo" + +#: .\contrib\localflavor\jp\jp_prefectures.py:17 +msgid "Kanagawa" +msgstr "Kanagawa" + +#: .\contrib\localflavor\jp\jp_prefectures.py:18 +msgid "Yamanashi" +msgstr "Yamanashi" + +#: .\contrib\localflavor\jp\jp_prefectures.py:19 +msgid "Nagano" +msgstr "Nagano" + +#: .\contrib\localflavor\jp\jp_prefectures.py:20 +msgid "Niigata" +msgstr "Niigata" + +#: .\contrib\localflavor\jp\jp_prefectures.py:21 +msgid "Toyama" +msgstr "Toyama" + +#: .\contrib\localflavor\jp\jp_prefectures.py:22 +msgid "Ishikawa" +msgstr "Ishikawa" + +#: .\contrib\localflavor\jp\jp_prefectures.py:23 +msgid "Fukui" +msgstr "Fukui" + +#: .\contrib\localflavor\jp\jp_prefectures.py:24 +msgid "Gifu" +msgstr "Gifu" + +#: .\contrib\localflavor\jp\jp_prefectures.py:25 +msgid "Shizuoka" +msgstr "Shizuoka" + +#: .\contrib\localflavor\jp\jp_prefectures.py:26 +msgid "Aichi" +msgstr "Aichi" + +#: .\contrib\localflavor\jp\jp_prefectures.py:27 +msgid "Mie" +msgstr "Mie" + +#: .\contrib\localflavor\jp\jp_prefectures.py:28 +msgid "Shiga" +msgstr "Shiga" + +#: .\contrib\localflavor\jp\jp_prefectures.py:29 +msgid "Kyoto" +msgstr "Kyoto" + +#: .\contrib\localflavor\jp\jp_prefectures.py:30 +msgid "Osaka" +msgstr "Osaka" + +#: .\contrib\localflavor\jp\jp_prefectures.py:31 +msgid "Hyogo" +msgstr "Hyogo" + +#: .\contrib\localflavor\jp\jp_prefectures.py:32 +msgid "Nara" +msgstr "Nara" + +#: .\contrib\localflavor\jp\jp_prefectures.py:33 +msgid "Wakayama" +msgstr "Wakayama" + +#: .\contrib\localflavor\jp\jp_prefectures.py:34 +msgid "Tottori" +msgstr "Tottori" + +#: .\contrib\localflavor\jp\jp_prefectures.py:35 +msgid "Shimane" +msgstr "Shimane" + +#: .\contrib\localflavor\jp\jp_prefectures.py:36 +msgid "Okayama" +msgstr "Okayama" + +#: .\contrib\localflavor\jp\jp_prefectures.py:37 +msgid "Hiroshima" +msgstr "Hiroshima" + +#: .\contrib\localflavor\jp\jp_prefectures.py:38 +msgid "Yamaguchi" +msgstr "Yamaguchi" + +#: .\contrib\localflavor\jp\jp_prefectures.py:39 +msgid "Tokushima" +msgstr "Tokushima" + +#: .\contrib\localflavor\jp\jp_prefectures.py:40 +msgid "Kagawa" +msgstr "Kagawa" + +#: .\contrib\localflavor\jp\jp_prefectures.py:41 +msgid "Ehime" +msgstr "Ehime" + +#: .\contrib\localflavor\jp\jp_prefectures.py:42 +msgid "Kochi" +msgstr "Kochi" + +#: .\contrib\localflavor\jp\jp_prefectures.py:43 +msgid "Fukuoka" +msgstr "Fukuoka" + +#: .\contrib\localflavor\jp\jp_prefectures.py:44 +msgid "Saga" +msgstr "Saga" + +#: .\contrib\localflavor\jp\jp_prefectures.py:45 +msgid "Nagasaki" +msgstr "Nagasaki" + +#: .\contrib\localflavor\jp\jp_prefectures.py:46 +msgid "Kumamoto" +msgstr "Kumamoto" + +#: .\contrib\localflavor\jp\jp_prefectures.py:47 +msgid "Oita" +msgstr "Oita" + +#: .\contrib\localflavor\jp\jp_prefectures.py:48 +msgid "Miyazaki" +msgstr "Miyazaki" + +#: .\contrib\localflavor\jp\jp_prefectures.py:49 +msgid "Kagoshima" +msgstr "Kagoshima" + +#: .\contrib\localflavor\jp\jp_prefectures.py:50 +msgid "Okinawa" +msgstr "Okinawa" + +#: .\contrib\localflavor\mx\mx_states.py:12 +msgid "Aguascalientes" +msgstr "Aguascalientes" + +#: .\contrib\localflavor\mx\mx_states.py:13 +msgid "Baja California" +msgstr "Baja California" + +#: .\contrib\localflavor\mx\mx_states.py:14 +msgid "Baja California Sur" +msgstr "Baja California Sur" + +#: .\contrib\localflavor\mx\mx_states.py:15 +msgid "Campeche" +msgstr "Campeche" + +#: .\contrib\localflavor\mx\mx_states.py:16 +msgid "Chihuahua" +msgstr "Chihuahua" + +#: .\contrib\localflavor\mx\mx_states.py:17 +msgid "Chiapas" +msgstr "Chiapas" + +#: .\contrib\localflavor\mx\mx_states.py:18 +msgid "Coahuila" +msgstr "Coahuila" + +#: .\contrib\localflavor\mx\mx_states.py:19 +msgid "Colima" +msgstr "Colima" + +#: .\contrib\localflavor\mx\mx_states.py:20 +msgid "Distrito Federal" +msgstr "Districtul Federal" + +#: .\contrib\localflavor\mx\mx_states.py:21 +msgid "Durango" +msgstr "Durango" + +#: .\contrib\localflavor\mx\mx_states.py:22 +msgid "Guerrero" +msgstr "Guerrero" + +#: .\contrib\localflavor\mx\mx_states.py:23 +msgid "Guanajuato" +msgstr "Guanajuato" + +#: .\contrib\localflavor\mx\mx_states.py:24 +msgid "Hidalgo" +msgstr "Hidalgo" + +#: .\contrib\localflavor\mx\mx_states.py:25 +msgid "Jalisco" +msgstr "Jalisco" + +#: .\contrib\localflavor\mx\mx_states.py:26 +msgid "Estado de México" +msgstr "Statul Mexico" + +#: .\contrib\localflavor\mx\mx_states.py:27 +msgid "Michoacán" +msgstr "Michoacán" + +#: .\contrib\localflavor\mx\mx_states.py:28 +msgid "Morelos" +msgstr "Morelos" + +#: .\contrib\localflavor\mx\mx_states.py:29 +msgid "Nayarit" +msgstr "Nayarit" + +#: .\contrib\localflavor\mx\mx_states.py:30 +msgid "Nuevo León" +msgstr "Nuevo León" + +#: .\contrib\localflavor\mx\mx_states.py:31 +msgid "Oaxaca" +msgstr "Oaxaca" + +#: .\contrib\localflavor\mx\mx_states.py:32 +msgid "Puebla" +msgstr "Puebla" + +#: .\contrib\localflavor\mx\mx_states.py:33 +msgid "Querétaro" +msgstr "Querétaro" + +#: .\contrib\localflavor\mx\mx_states.py:34 +msgid "Quintana Roo" +msgstr "Quintana Roo" + +#: .\contrib\localflavor\mx\mx_states.py:35 +msgid "Sinaloa" +msgstr "Sinaloa" + +#: .\contrib\localflavor\mx\mx_states.py:36 +msgid "San Luis Potosí" +msgstr "San Luis Potosí" + +#: .\contrib\localflavor\mx\mx_states.py:37 +msgid "Sonora" +msgstr "Sonora" + +#: .\contrib\localflavor\mx\mx_states.py:38 +msgid "Tabasco" +msgstr "Tabasco" + +#: .\contrib\localflavor\mx\mx_states.py:39 +msgid "Tamaulipas" +msgstr "Tamaulipas" + +#: .\contrib\localflavor\mx\mx_states.py:40 +msgid "Tlaxcala" +msgstr "Tlaxcala" + +#: .\contrib\localflavor\mx\mx_states.py:41 +msgid "Veracruz" +msgstr "Veracruz" + +#: .\contrib\localflavor\mx\mx_states.py:42 +msgid "Yucatán" +msgstr "Yucatan" + +#: .\contrib\localflavor\mx\mx_states.py:43 +msgid "Zacatecas" +msgstr "Zacatecas" + +#: .\contrib\localflavor\nl\forms.py:21 +msgid "Enter a valid postal code" +msgstr "Introduceti un cod postal valid" + +#: .\contrib\localflavor\nl\forms.py:52 +msgid "Enter a valid phone number" +msgstr "Introduceţi un număr îde telefon valid" + +#: .\contrib\localflavor\nl\forms.py:78 +msgid "Enter a valid SoFi number" +msgstr "Introduceti un numar SoFi valid" + +#: .\contrib\localflavor\nl\nl_provinces.py:4 +msgid "Drente" +msgstr "Drente" + +#: .\contrib\localflavor\nl\nl_provinces.py:5 +msgid "Flevoland" +msgstr "Flevoland" + +#: .\contrib\localflavor\nl\nl_provinces.py:6 +msgid "Friesland" +msgstr "Frizia" + +#: .\contrib\localflavor\nl\nl_provinces.py:7 +msgid "Gelderland" +msgstr "Gelderland" + +#: .\contrib\localflavor\nl\nl_provinces.py:8 +msgid "Groningen" +msgstr "Groningen" + +#: .\contrib\localflavor\nl\nl_provinces.py:9 +msgid "Limburg" +msgstr "Limburg" + +#: .\contrib\localflavor\nl\nl_provinces.py:10 +msgid "Noord-Brabant" +msgstr "Brabantul de Nord" + +#: .\contrib\localflavor\nl\nl_provinces.py:11 +msgid "Noord-Holland" +msgstr "Olanda de Nord" + +#: .\contrib\localflavor\nl\nl_provinces.py:12 +msgid "Overijssel" +msgstr "Overijssel" + +#: .\contrib\localflavor\nl\nl_provinces.py:13 +msgid "Utrecht" +msgstr "Utrecht" + +#: .\contrib\localflavor\nl\nl_provinces.py:14 +msgid "Zeeland" +msgstr "Zeelanda" + +#: .\contrib\localflavor\nl\nl_provinces.py:15 +msgid "Zuid-Holland" +msgstr "Olanda de Sud" + +#: .\contrib\localflavor\no\forms.py:33 +msgid "Enter a valid Norwegian social security number." +msgstr "Introduceti un numar norvegian de securitate sociala valid." + +#: .\contrib\localflavor\pe\forms.py:24 +msgid "This field requires 8 digits." +msgstr "Acest camp are nevoie de 8 cifre." + +#: .\contrib\localflavor\pe\forms.py:52 +msgid "This field requires 11 digits." +msgstr "Acest camp are nevoie de 11 cifre." + +#: .\contrib\localflavor\pl\forms.py:39 +msgid "National Identification Number consists of 11 digits." +msgstr "Numarul National de Identificare contine 11 cifre." + +#: .\contrib\localflavor\pl\forms.py:40 +msgid "Wrong checksum for the National Identification Number." +msgstr "Suma de control gresita pentru Numarul National de Identificare." + +#: .\contrib\localflavor\pl\forms.py:72 +msgid "Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX." +msgstr "Introduceti un numar de taxa(NIP) de forma XXX-XXX-XX-XX sau XX-XX-XXX-XXX." + +#: .\contrib\localflavor\pl\forms.py:73 +msgid "Wrong checksum for the Tax Number (NIP)." +msgstr "Suma de control gresita pentru Numarul de Taxa (NIP)." + +#: .\contrib\localflavor\pl\forms.py:112 +msgid "National Business Register Number (REGON) consists of 7 or 9 digits." +msgstr "Numarul National din Registrul pentru Afaceri (REGON) contine 7 sau 9 cifre." + +#: .\contrib\localflavor\pl\forms.py:113 +msgid "Wrong checksum for the National Business Register Number (REGON)." +msgstr "Suma de control gresita pentru Numarul National din Registrul pentru Afaceri (REGON)." + +#: .\contrib\localflavor\pl\forms.py:156 +msgid "Enter a postal code in the format XX-XXX." +msgstr "Introduceti un cod postal de forma XX-XXX." + +#: .\contrib\localflavor\pl\pl_voivodeships.py:8 +msgid "Lower Silesia" +msgstr "Silezia Inferioara" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:9 +msgid "Kuyavia-Pomerania" +msgstr "Kuyavia-Pomerania" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:10 +msgid "Lublin" +msgstr "Lublin" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:11 +msgid "Lubusz" +msgstr "Lubusz" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:12 +msgid "Lodz" +msgstr "Lodz" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:13 +msgid "Lesser Poland" +msgstr "Polonia Mica" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:14 +msgid "Masovia" +msgstr "Masovia" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:15 +msgid "Opole" +msgstr "Opole" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:16 +msgid "Subcarpatia" +msgstr "Podcarpatia" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:17 +msgid "Podlasie" +msgstr "Podlasie" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:18 +msgid "Pomerania" +msgstr "Pomerania" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:19 +msgid "Silesia" +msgstr "Silezia" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:20 +msgid "Swietokrzyskie" +msgstr "Swietokrzyskie" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:21 +msgid "Warmia-Masuria" +msgstr "Warmia-Masuria" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:22 +msgid "Greater Poland" +msgstr "Polonia Mare" + +#: .\contrib\localflavor\pl\pl_voivodeships.py:23 +msgid "West Pomerania" +msgstr "Pomerania Occidentala" + +#: .\contrib\localflavor\sk\forms.py:30 +msgid "Enter a postal code in the format XXXXX or XXX XX." +msgstr "Introduceti un cod postal de forma XXXXX or XXX XX." + +#: .\contrib\localflavor\sk\sk_districts.py:8 +msgid "Banska Bystrica" +msgstr "Banska Bystrica" + +#: .\contrib\localflavor\sk\sk_districts.py:9 +msgid "Banska Stiavnica" +msgstr "Banska Stiavnica" + +#: .\contrib\localflavor\sk\sk_districts.py:10 +msgid "Bardejov" +msgstr "Bardejov" + +#: .\contrib\localflavor\sk\sk_districts.py:11 +msgid "Banovce nad Bebravou" +msgstr "Banovce nad Bebravou" + +#: .\contrib\localflavor\sk\sk_districts.py:12 +msgid "Brezno" +msgstr "Brezno" + +#: .\contrib\localflavor\sk\sk_districts.py:13 +msgid "Bratislava I" +msgstr "Bratislava I" + +#: .\contrib\localflavor\sk\sk_districts.py:14 +msgid "Bratislava II" +msgstr "Bratislava II" + +#: .\contrib\localflavor\sk\sk_districts.py:15 +msgid "Bratislava III" +msgstr "Bratislava III" + +#: .\contrib\localflavor\sk\sk_districts.py:16 +msgid "Bratislava IV" +msgstr "Bratislava IV" + +#: .\contrib\localflavor\sk\sk_districts.py:17 +msgid "Bratislava V" +msgstr "Bratislava V" + +#: .\contrib\localflavor\sk\sk_districts.py:18 +msgid "Bytca" +msgstr "Bytca" + +#: .\contrib\localflavor\sk\sk_districts.py:19 +msgid "Cadca" +msgstr "Cadca" + +#: .\contrib\localflavor\sk\sk_districts.py:20 +msgid "Detva" +msgstr "Detva" + +#: .\contrib\localflavor\sk\sk_districts.py:21 +msgid "Dolny Kubin" +msgstr "Dolny Kubin" + +#: .\contrib\localflavor\sk\sk_districts.py:22 +msgid "Dunajska Streda" +msgstr "Dunajska Streda" + +#: .\contrib\localflavor\sk\sk_districts.py:23 +msgid "Galanta" +msgstr "Galanta" + +#: .\contrib\localflavor\sk\sk_districts.py:24 +msgid "Gelnica" +msgstr "Gelnica" + +#: .\contrib\localflavor\sk\sk_districts.py:25 +msgid "Hlohovec" +msgstr "Hlohovec" + +#: .\contrib\localflavor\sk\sk_districts.py:26 +msgid "Humenne" +msgstr "Humenne" + +#: .\contrib\localflavor\sk\sk_districts.py:27 +msgid "Ilava" +msgstr "Ilava" + +#: .\contrib\localflavor\sk\sk_districts.py:28 +msgid "Kezmarok" +msgstr "Kezmarok" + +#: .\contrib\localflavor\sk\sk_districts.py:29 +msgid "Komarno" +msgstr "Komarno" + +#: .\contrib\localflavor\sk\sk_districts.py:30 +msgid "Kosice I" +msgstr "Kosice I" + +#: .\contrib\localflavor\sk\sk_districts.py:31 +msgid "Kosice II" +msgstr "Kosice II" + +#: .\contrib\localflavor\sk\sk_districts.py:32 +msgid "Kosice III" +msgstr "Kosice III" + +#: .\contrib\localflavor\sk\sk_districts.py:33 +msgid "Kosice IV" +msgstr "Kosice IV" + +#: .\contrib\localflavor\sk\sk_districts.py:34 +msgid "Kosice - okolie" +msgstr "Kosice - okolie" + +#: .\contrib\localflavor\sk\sk_districts.py:35 +msgid "Krupina" +msgstr "Krupina" + +#: .\contrib\localflavor\sk\sk_districts.py:36 +msgid "Kysucke Nove Mesto" +msgstr "Kysucke Nove Mesto" + +#: .\contrib\localflavor\sk\sk_districts.py:37 +msgid "Levice" +msgstr "Levice" + +#: .\contrib\localflavor\sk\sk_districts.py:38 +msgid "Levoca" +msgstr "Levoca" + +#: .\contrib\localflavor\sk\sk_districts.py:39 +msgid "Liptovsky Mikulas" +msgstr "Liptovsky Mikulas" + +#: .\contrib\localflavor\sk\sk_districts.py:40 +msgid "Lucenec" +msgstr "Lucenec" + +#: .\contrib\localflavor\sk\sk_districts.py:41 +msgid "Malacky" +msgstr "Malacky" + +#: .\contrib\localflavor\sk\sk_districts.py:42 +msgid "Martin" +msgstr "Martin" + +#: .\contrib\localflavor\sk\sk_districts.py:43 +msgid "Medzilaborce" +msgstr "Medzilaborce" + +#: .\contrib\localflavor\sk\sk_districts.py:44 +msgid "Michalovce" +msgstr "Michalovce" + +#: .\contrib\localflavor\sk\sk_districts.py:45 +msgid "Myjava" +msgstr "Myjava" + +#: .\contrib\localflavor\sk\sk_districts.py:46 +msgid "Namestovo" +msgstr "Namestovo" + +#: .\contrib\localflavor\sk\sk_districts.py:47 +msgid "Nitra" +msgstr "Nitra" + +#: .\contrib\localflavor\sk\sk_districts.py:48 +msgid "Nove Mesto nad Vahom" +msgstr "Nove Mesto nad Vahom" + +#: .\contrib\localflavor\sk\sk_districts.py:49 +msgid "Nove Zamky" +msgstr "Nove Zamky" + +#: .\contrib\localflavor\sk\sk_districts.py:50 +msgid "Partizanske" +msgstr "Partizanske" + +#: .\contrib\localflavor\sk\sk_districts.py:51 +msgid "Pezinok" +msgstr "Pezinok" + +#: .\contrib\localflavor\sk\sk_districts.py:52 +msgid "Piestany" +msgstr "Piestany" + +#: .\contrib\localflavor\sk\sk_districts.py:53 +msgid "Poltar" +msgstr "Poltar" + +#: .\contrib\localflavor\sk\sk_districts.py:54 +msgid "Poprad" +msgstr "Poprad" + +#: .\contrib\localflavor\sk\sk_districts.py:55 +msgid "Povazska Bystrica" +msgstr "Povazska Bystrica" + +#: .\contrib\localflavor\sk\sk_districts.py:56 +msgid "Presov" +msgstr "Presov" + +#: .\contrib\localflavor\sk\sk_districts.py:57 +msgid "Prievidza" +msgstr "Prievidza" + +#: .\contrib\localflavor\sk\sk_districts.py:58 +msgid "Puchov" +msgstr "Puchov" + +#: .\contrib\localflavor\sk\sk_districts.py:59 +msgid "Revuca" +msgstr "Revuca" + +#: .\contrib\localflavor\sk\sk_districts.py:60 +msgid "Rimavska Sobota" +msgstr "Rimavska Sobota" + +#: .\contrib\localflavor\sk\sk_districts.py:61 +msgid "Roznava" +msgstr "Roznava" + +#: .\contrib\localflavor\sk\sk_districts.py:62 +msgid "Ruzomberok" +msgstr "Ruzomberok" + +#: .\contrib\localflavor\sk\sk_districts.py:63 +msgid "Sabinov" +msgstr "Sabinov" + +#: .\contrib\localflavor\sk\sk_districts.py:64 +msgid "Senec" +msgstr "Senec" + +#: .\contrib\localflavor\sk\sk_districts.py:65 +msgid "Senica" +msgstr "Senica" + +#: .\contrib\localflavor\sk\sk_districts.py:66 +msgid "Skalica" +msgstr "Skalica" + +#: .\contrib\localflavor\sk\sk_districts.py:67 +msgid "Snina" +msgstr "Snina" + +#: .\contrib\localflavor\sk\sk_districts.py:68 +msgid "Sobrance" +msgstr "Sobrance" + +#: .\contrib\localflavor\sk\sk_districts.py:69 +msgid "Spisska Nova Ves" +msgstr "Spisska Nova Ves" + +#: .\contrib\localflavor\sk\sk_districts.py:70 +msgid "Stara Lubovna" +msgstr "Stara Lubovna" + +#: .\contrib\localflavor\sk\sk_districts.py:71 +msgid "Stropkov" +msgstr "Stropkov" + +#: .\contrib\localflavor\sk\sk_districts.py:72 +msgid "Svidnik" +msgstr "Svidnik" + +#: .\contrib\localflavor\sk\sk_districts.py:73 +msgid "Sala" +msgstr "Sala" + +#: .\contrib\localflavor\sk\sk_districts.py:74 +msgid "Topolcany" +msgstr "Topolcany" + +#: .\contrib\localflavor\sk\sk_districts.py:75 +msgid "Trebisov" +msgstr "Trebisov" + +#: .\contrib\localflavor\sk\sk_districts.py:76 +msgid "Trencin" +msgstr "Trencin" + +#: .\contrib\localflavor\sk\sk_districts.py:77 +msgid "Trnava" +msgstr "Trnava" + +#: .\contrib\localflavor\sk\sk_districts.py:78 +msgid "Turcianske Teplice" +msgstr "Turcianske Teplice" + +#: .\contrib\localflavor\sk\sk_districts.py:79 +msgid "Tvrdosin" +msgstr "Tvrdosin" + +#: .\contrib\localflavor\sk\sk_districts.py:80 +msgid "Velky Krtis" +msgstr "Velky Krtis" + +#: .\contrib\localflavor\sk\sk_districts.py:81 +msgid "Vranov nad Toplou" +msgstr "Vranov nad Toplou" + +#: .\contrib\localflavor\sk\sk_districts.py:82 +msgid "Zlate Moravce" +msgstr "Zlate Moravce" + +#: .\contrib\localflavor\sk\sk_districts.py:83 +msgid "Zvolen" +msgstr "Zvolen" + +#: .\contrib\localflavor\sk\sk_districts.py:84 +msgid "Zarnovica" +msgstr "Zarnovica" + +#: .\contrib\localflavor\sk\sk_districts.py:85 +msgid "Ziar nad Hronom" +msgstr "Ziar nad Hronom" + +#: .\contrib\localflavor\sk\sk_districts.py:86 +msgid "Zilina" +msgstr "Zilina" + +#: .\contrib\localflavor\sk\sk_regions.py:8 +msgid "Banska Bystrica region" +msgstr "Banska Bystrica region" + +#: .\contrib\localflavor\sk\sk_regions.py:9 +msgid "Bratislava region" +msgstr "Regiunea Bratislava" + +#: .\contrib\localflavor\sk\sk_regions.py:10 +msgid "Kosice region" +msgstr "Regiunea Kosice" + +#: .\contrib\localflavor\sk\sk_regions.py:11 +msgid "Nitra region" +msgstr "Regiunea Nitra" + +#: .\contrib\localflavor\sk\sk_regions.py:12 +msgid "Presov region" +msgstr "Regiunea Presov" + +#: .\contrib\localflavor\sk\sk_regions.py:13 +msgid "Trencin region" +msgstr "Regiunea Trencin" + +#: .\contrib\localflavor\sk\sk_regions.py:14 +msgid "Trnava region" +msgstr "Regiunea Trnava" + +#: .\contrib\localflavor\sk\sk_regions.py:15 +msgid "Zilina region" +msgstr "Regiunea Zilina" + +#: .\contrib\localflavor\uk\forms.py:21 +msgid "Enter a valid postcode." +msgstr "Introduceti un cod postal valid." + +#: .\contrib\localflavor\uk\uk_regions.py:11 +msgid "Bedfordshire" +msgstr "Bedfordshire" + +#: .\contrib\localflavor\uk\uk_regions.py:12 +msgid "Buckinghamshire" +msgstr "Buckinghamshire" + +#: .\contrib\localflavor\uk\uk_regions.py:14 +msgid "Cheshire" +msgstr "Cheshire" + +#: .\contrib\localflavor\uk\uk_regions.py:15 +msgid "Cornwall and Isles of Scilly" +msgstr "Cornwall si Insulele Scilly" + +#: .\contrib\localflavor\uk\uk_regions.py:16 +msgid "Cumbria" +msgstr "Cumbria" + +#: .\contrib\localflavor\uk\uk_regions.py:17 +msgid "Derbyshire" +msgstr "Derbyshire" + +#: .\contrib\localflavor\uk\uk_regions.py:18 +msgid "Devon" +msgstr "Devon" + +#: .\contrib\localflavor\uk\uk_regions.py:19 +msgid "Dorset" +msgstr "Dorset" + +#: .\contrib\localflavor\uk\uk_regions.py:20 +msgid "Durham" +msgstr "Durham" + +#: .\contrib\localflavor\uk\uk_regions.py:21 +msgid "East Sussex" +msgstr "East Sussex" + +#: .\contrib\localflavor\uk\uk_regions.py:22 +msgid "Essex" +msgstr "Essex" + +#: .\contrib\localflavor\uk\uk_regions.py:23 +msgid "Gloucestershire" +msgstr "Gloucestershire" + +#: .\contrib\localflavor\uk\uk_regions.py:24 +msgid "Greater London" +msgstr "Londra Mare" + +#: .\contrib\localflavor\uk\uk_regions.py:25 +msgid "Greater Manchester" +msgstr "Greater Manchester" + +#: .\contrib\localflavor\uk\uk_regions.py:26 +msgid "Hampshire" +msgstr "Hampshire" + +#: .\contrib\localflavor\uk\uk_regions.py:27 +msgid "Hertfordshire" +msgstr "Hertfordshire" + +#: .\contrib\localflavor\uk\uk_regions.py:28 +msgid "Kent" +msgstr "Kent" + +#: .\contrib\localflavor\uk\uk_regions.py:29 +msgid "Lancashire" +msgstr "Lancashire" + +#: .\contrib\localflavor\uk\uk_regions.py:30 +msgid "Leicestershire" +msgstr "Leicestershire" + +#: .\contrib\localflavor\uk\uk_regions.py:31 +msgid "Lincolnshire" +msgstr "Lincolnshire" + +#: .\contrib\localflavor\uk\uk_regions.py:32 +msgid "Merseyside" +msgstr "Merseyside" + +#: .\contrib\localflavor\uk\uk_regions.py:33 +msgid "Norfolk" +msgstr "Norfolk" + +#: .\contrib\localflavor\uk\uk_regions.py:34 +msgid "North Yorkshire" +msgstr "North Yorkshire" + +#: .\contrib\localflavor\uk\uk_regions.py:35 +msgid "Northamptonshire" +msgstr "Northamptonshire" + +#: .\contrib\localflavor\uk\uk_regions.py:36 +msgid "Northumberland" +msgstr "Northumberland" + +#: .\contrib\localflavor\uk\uk_regions.py:37 +msgid "Nottinghamshire" +msgstr "Nottinghamshire" + +#: .\contrib\localflavor\uk\uk_regions.py:38 +msgid "Oxfordshire" +msgstr "Oxfordshire" + +#: .\contrib\localflavor\uk\uk_regions.py:39 +msgid "Shropshire" +msgstr "Shropshire" + +#: .\contrib\localflavor\uk\uk_regions.py:40 +msgid "Somerset" +msgstr "Somerset" + +#: .\contrib\localflavor\uk\uk_regions.py:41 +msgid "South Yorkshire" +msgstr "South Yorkshire" + +#: .\contrib\localflavor\uk\uk_regions.py:42 +msgid "Staffordshire" +msgstr "Staffordshire" + +#: .\contrib\localflavor\uk\uk_regions.py:43 +msgid "Suffolk" +msgstr "Suffolk" + +#: .\contrib\localflavor\uk\uk_regions.py:44 +msgid "Surrey" +msgstr "Surrey" + +#: .\contrib\localflavor\uk\uk_regions.py:45 +msgid "Tyne and Wear" +msgstr "Tyne si Wear" + +#: .\contrib\localflavor\uk\uk_regions.py:46 +msgid "Warwickshire" +msgstr "Warwickshire" + +#: .\contrib\localflavor\uk\uk_regions.py:47 +msgid "West Midlands" +msgstr "West Midlands" + +#: .\contrib\localflavor\uk\uk_regions.py:48 +msgid "West Sussex" +msgstr "West Sussex" + +#: .\contrib\localflavor\uk\uk_regions.py:49 +msgid "West Yorkshire" +msgstr "West Yorkshire" + +#: .\contrib\localflavor\uk\uk_regions.py:50 +msgid "Wiltshire" +msgstr "Wiltshire" + +#: .\contrib\localflavor\uk\uk_regions.py:51 +msgid "Worcestershire" +msgstr "Worcestershire" + +#: .\contrib\localflavor\uk\uk_regions.py:55 +msgid "County Antrim" +msgstr "Comitatul Antrim" + +#: .\contrib\localflavor\uk\uk_regions.py:56 +msgid "County Armagh" +msgstr "Comitatul Armagh" + +#: .\contrib\localflavor\uk\uk_regions.py:57 +msgid "County Down" +msgstr "Comitatul Down" + +#: .\contrib\localflavor\uk\uk_regions.py:58 +msgid "County Fermanagh" +msgstr "Comitatul Fermanagh" + +#: .\contrib\localflavor\uk\uk_regions.py:59 +msgid "County Londonderry" +msgstr "Comitatul Londonderry" + +#: .\contrib\localflavor\uk\uk_regions.py:60 +msgid "County Tyrone" +msgstr "Comitatul Tyrone" + +#: .\contrib\localflavor\uk\uk_regions.py:64 +msgid "Clwyd" +msgstr "Clwyd" + +#: .\contrib\localflavor\uk\uk_regions.py:65 +msgid "Dyfed" +msgstr "Dyfed" + +#: .\contrib\localflavor\uk\uk_regions.py:66 +msgid "Gwent" +msgstr "Gwent" + +#: .\contrib\localflavor\uk\uk_regions.py:67 +msgid "Gwynedd" +msgstr "Gwynedd" + +#: .\contrib\localflavor\uk\uk_regions.py:68 +msgid "Mid Glamorgan" +msgstr "Mid Glamorgan" + +#: .\contrib\localflavor\uk\uk_regions.py:69 +msgid "Powys" +msgstr "Powys" + +#: .\contrib\localflavor\uk\uk_regions.py:70 +msgid "South Glamorgan" +msgstr "South Glamorgan" + +#: .\contrib\localflavor\uk\uk_regions.py:71 +msgid "West Glamorgan" +msgstr "West Glamorgan" + +#: .\contrib\localflavor\uk\uk_regions.py:75 +msgid "Borders" +msgstr "Borders" + +#: .\contrib\localflavor\uk\uk_regions.py:76 +msgid "Central Scotland" +msgstr "Central Scotland" + +#: .\contrib\localflavor\uk\uk_regions.py:77 +msgid "Dumfries and Galloway" +msgstr "Dumfries si Galloway" + +#: .\contrib\localflavor\uk\uk_regions.py:78 +msgid "Fife" +msgstr "Fife" + +#: .\contrib\localflavor\uk\uk_regions.py:79 +msgid "Grampian" +msgstr "Grampian" + +#: .\contrib\localflavor\uk\uk_regions.py:80 +msgid "Highland" +msgstr "Highland" + +#: .\contrib\localflavor\uk\uk_regions.py:81 +msgid "Lothian" +msgstr "Lothian" + +#: .\contrib\localflavor\uk\uk_regions.py:82 +msgid "Orkney Islands" +msgstr "Orkney Islands" + +#: .\contrib\localflavor\uk\uk_regions.py:83 +msgid "Shetland Islands" +msgstr "Shetland Islands" + +#: .\contrib\localflavor\uk\uk_regions.py:84 +msgid "Strathclyde" +msgstr "Strathclyde" + +#: .\contrib\localflavor\uk\uk_regions.py:85 +msgid "Tayside" +msgstr "Tayside" + +#: .\contrib\localflavor\uk\uk_regions.py:86 +msgid "Western Isles" +msgstr "Western Isles" + +#: .\contrib\localflavor\uk\uk_regions.py:90 +msgid "England" +msgstr "Anglia" + +#: .\contrib\localflavor\uk\uk_regions.py:91 +msgid "Northern Ireland" +msgstr "Irlanda de Nord" + +#: .\contrib\localflavor\uk\uk_regions.py:92 +msgid "Scotland" +msgstr "Scotia" + +#: .\contrib\localflavor\uk\uk_regions.py:93 +msgid "Wales" +msgstr "Tara Galilor" + +#: .\contrib\localflavor\us\forms.py:16 +msgid "Enter a zip code in the format XXXXX or XXXXX-XXXX." +msgstr "Introduceti un cod postal de forma XXXXX or XXXXX-XXXX." + +#: .\contrib\localflavor\us\forms.py:54 +msgid "Enter a valid U.S. Social Security number in XXX-XX-XXXX format." +msgstr "Introduceti un numar SUA de Securitate Sociala de forma XXX-XX-XXXX format." + +#: .\contrib\localflavor\za\forms.py:20 +msgid "Enter a valid South African ID number" +msgstr "Introduceti un ID sud african valid" + +#: .\contrib\localflavor\za\forms.py:54 +msgid "Enter a valid South African postal code" +msgstr "Introduceti un cod postal sud african valid" + +#: .\contrib\localflavor\za\za_provinces.py:4 +msgid "Eastern Cape" +msgstr "Eastern Cape" + +#: .\contrib\localflavor\za\za_provinces.py:5 +msgid "Free State" +msgstr "Free State" + +#: .\contrib\localflavor\za\za_provinces.py:6 +msgid "Gauteng" +msgstr "Gauteng" + +#: .\contrib\localflavor\za\za_provinces.py:7 +msgid "KwaZulu-Natal" +msgstr "KwaZulu-Natal" + +#: .\contrib\localflavor\za\za_provinces.py:8 +msgid "Limpopo" +msgstr "Limpopo" + +#: .\contrib\localflavor\za\za_provinces.py:9 +msgid "Mpumalanga" +msgstr "Mpumalanga" + +#: .\contrib\localflavor\za\za_provinces.py:10 +msgid "Northern Cape" +msgstr "Northern Cape" + +#: .\contrib\localflavor\za\za_provinces.py:11 +msgid "North West" +msgstr "North West" + +#: .\contrib\localflavor\za\za_provinces.py:12 +msgid "Western Cape" +msgstr "Western Cape" + +#: .\contrib\redirects\models.py:7 +msgid "redirect from" +msgstr "redirectat de la " + +#: .\contrib\redirects\models.py:8 +msgid "This should be an absolute path, excluding the domain name. Example: '/events/search/'." +msgstr "Aceasta ar trebui să fie o cale absolută, excluzînd numele de domeniu. Exemplu: '/evenimente/cautare/'." + +#: .\contrib\redirects\models.py:9 +msgid "redirect to" +msgstr "redirectat la" + +#: .\contrib\redirects\models.py:10 +msgid "This can be either an absolute path (as above) or a full URL starting with 'http://'." +msgstr "Aceasta poate fi o cale absolută (ca mai sus) sau un URL începînd cu 'http://'." + +#: .\contrib\redirects\models.py:13 +msgid "redirect" +msgstr "redirectare" + +#: .\contrib\redirects\models.py:14 +msgid "redirects" +msgstr "redictări" + +#: .\contrib\sessions\models.py:41 msgid "session key" msgstr "cheie sesiune" -#: contrib/sessions/models.py:36 +#: .\contrib\sessions\models.py:42 msgid "session data" msgstr "date sesiune" -#: contrib/sessions/models.py:37 +#: .\contrib\sessions\models.py:43 msgid "expire date" msgstr "data expirare" -#: contrib/sessions/models.py:41 +#: .\contrib\sessions\models.py:48 msgid "session" msgstr "sesiune" -#: contrib/sessions/models.py:42 +#: .\contrib\sessions\models.py:49 msgid "sessions" msgstr "sesiuni" -#: contrib/sites/models.py:10 +#: .\contrib\sites\models.py:32 msgid "domain name" msgstr "nume domeniu" -#: contrib/sites/models.py:11 +#: .\contrib\sites\models.py:33 msgid "display name" msgstr "nume afişat" -#: contrib/sites/models.py:15 +#: .\contrib\sites\models.py:37 msgid "site" msgstr "sit" -#: contrib/sites/models.py:16 +#: .\contrib\sites\models.py:38 msgid "sites" msgstr "sit" -#: utils/translation.py:360 -msgid "DATE_FORMAT" -msgstr "" - -#: utils/translation.py:361 -msgid "DATETIME_FORMAT" -msgstr "" - -#: utils/translation.py:362 -msgid "TIME_FORMAT" -msgstr "" - -#: utils/dates.py:6 -msgid "Monday" -msgstr "Luni" - -#: utils/dates.py:6 -msgid "Tuesday" -msgstr "Marţi" - -#: utils/dates.py:6 -msgid "Wednesday" -msgstr "Miercuri" - -#: utils/dates.py:6 -msgid "Thursday" -msgstr "Joi" - -#: utils/dates.py:6 -msgid "Friday" -msgstr "Vineri" - -#: utils/dates.py:7 -msgid "Saturday" -msgstr "Sîmbătă" - -#: utils/dates.py:7 -msgid "Sunday" -msgstr "Duminică" - -#: utils/dates.py:14 -msgid "January" -msgstr "Ianuarie" - -#: utils/dates.py:14 -msgid "February" -msgstr "Februarie" - -#: utils/dates.py:14 utils/dates.py:27 -msgid "March" -msgstr "Martie" - -#: utils/dates.py:14 utils/dates.py:27 -msgid "April" -msgstr "Aprilie" - -#: utils/dates.py:14 utils/dates.py:27 -msgid "May" -msgstr "Mai" - -#: utils/dates.py:14 utils/dates.py:27 -msgid "June" -msgstr "Iunie" - -#: utils/dates.py:15 utils/dates.py:27 -msgid "July" -msgstr "Iulie" - -#: utils/dates.py:15 -msgid "August" -msgstr "August" - -#: utils/dates.py:15 -msgid "September" -msgstr "Septembrie" - -#: utils/dates.py:15 -msgid "October" -msgstr "Octombrie" - -#: utils/dates.py:15 -msgid "November" -msgstr "Noiembrie" - -#: utils/dates.py:16 -msgid "December" -msgstr "Decembrie" - -#: utils/dates.py:19 -msgid "jan" -msgstr "ian" - -#: utils/dates.py:19 -msgid "feb" -msgstr "" - -#: utils/dates.py:19 -msgid "mar" -msgstr "" - -#: utils/dates.py:19 -msgid "apr" -msgstr "" - -#: utils/dates.py:19 -msgid "may" -msgstr "mai" - -#: utils/dates.py:19 -msgid "jun" -msgstr "iun" - -#: utils/dates.py:20 -msgid "jul" -msgstr "iul" - -#: utils/dates.py:20 -msgid "aug" -msgstr "" - -#: utils/dates.py:20 -msgid "sep" -msgstr "" - -#: utils/dates.py:20 -msgid "oct" -msgstr "" - -#: utils/dates.py:20 -msgid "nov" -msgstr "noi" - -#: utils/dates.py:20 -msgid "dec" -msgstr "" - -#: utils/dates.py:27 -msgid "Jan." -msgstr "Ian." - -#: utils/dates.py:27 -msgid "Feb." -msgstr "Feb." - -#: utils/dates.py:28 -msgid "Aug." -msgstr "Aug." - -#: utils/dates.py:28 -msgid "Sept." -msgstr "Sept." - -#: utils/dates.py:28 -msgid "Oct." -msgstr "Oct" - -#: utils/dates.py:28 -msgid "Nov." -msgstr "Noi." - -#: utils/dates.py:28 -msgid "Dec." -msgstr "Dec." - -#: utils/timesince.py:12 -msgid "year" -msgid_plural "years" -msgstr[0] "an" -msgstr[1] "ani" - -#: utils/timesince.py:13 -msgid "month" -msgid_plural "months" -msgstr[0] "luna" -msgstr[1] "luni" - -#: utils/timesince.py:14 -msgid "week" -msgid_plural "weeks" -msgstr[0] "saptamana" -msgstr[1] "saptamani" - -#: utils/timesince.py:15 -msgid "day" -msgid_plural "days" -msgstr[0] "zi" -msgstr[1] "zile" - -#: utils/timesince.py:16 -msgid "hour" -msgid_plural "hours" -msgstr[0] "ora" -msgstr[1] "ore" - -#: utils/timesince.py:17 -msgid "minute" -msgid_plural "minutes" -msgstr[0] "minut" -msgstr[1] "minute" - -#: conf/global_settings.py:37 -msgid "Bengali" -msgstr "" - -#: conf/global_settings.py:38 -msgid "Czech" -msgstr "Cehă" - -#: conf/global_settings.py:39 -msgid "Welsh" -msgstr "" - -#: conf/global_settings.py:40 -#, fuzzy -msgid "Danish" -msgstr "Spaniolă" - -#: conf/global_settings.py:41 -msgid "German" -msgstr "Germană" - -#: conf/global_settings.py:42 -msgid "Greek" -msgstr "" - -#: conf/global_settings.py:43 -msgid "English" -msgstr "Engleză" - -#: conf/global_settings.py:44 -msgid "Spanish" -msgstr "Spaniolă" - -#: conf/global_settings.py:45 -msgid "French" -msgstr "Franceză" - -#: conf/global_settings.py:46 -msgid "Galician" -msgstr "Galiciană" - -#: conf/global_settings.py:47 -msgid "Hungarian" -msgstr "Ungara" - -#: conf/global_settings.py:48 -msgid "Hebrew" -msgstr "" - -#: conf/global_settings.py:49 -msgid "Icelandic" -msgstr "" - -#: conf/global_settings.py:50 -msgid "Italian" -msgstr "Italiană" - -#: conf/global_settings.py:51 -msgid "Japanese" -msgstr "" - -#: conf/global_settings.py:52 -msgid "Dutch" -msgstr "" - -#: conf/global_settings.py:53 -msgid "Norwegian" -msgstr "Norvegiană" - -#: conf/global_settings.py:54 -msgid "Brazilian" -msgstr "Braziliană" - -#: conf/global_settings.py:55 -msgid "Romanian" -msgstr "Romana" - -#: conf/global_settings.py:56 -msgid "Russian" -msgstr "Rusă" - -#: conf/global_settings.py:57 -msgid "Slovak" -msgstr "" - -#: conf/global_settings.py:58 -#, fuzzy -msgid "Slovenian" -msgstr "Sîrbă" - -#: conf/global_settings.py:59 -msgid "Serbian" -msgstr "Sîrbă" - -#: conf/global_settings.py:60 -msgid "Swedish" -msgstr "" - -#: conf/global_settings.py:61 -#, fuzzy -msgid "Ukrainian" -msgstr "Braziliană" - -#: conf/global_settings.py:62 -msgid "Simplified Chinese" -msgstr "Chineză simplificată" - -#: conf/global_settings.py:63 -msgid "Traditional Chinese" -msgstr "" - -#: core/validators.py:60 +#: .\core\validators.py:72 msgid "This value must contain only letters, numbers and underscores." -msgstr "" -"Această valoare trebuie să conţină numai litere, numere şi liniuţe de " -"subliniere." +msgstr "Această valoare trebuie să conţină numai litere, numere şi liniuţe de subliniere." -#: core/validators.py:64 -#, fuzzy -msgid "" -"This value must contain only letters, numbers, underscores, dashes or " -"slashes." -msgstr "" -"Această valoare trebuie să conţină numai litere, numere, liniuţe de " -"subliniere şi slash-uri." +#: .\core\validators.py:76 +msgid "This value must contain only letters, numbers, underscores, dashes or slashes." +msgstr "Această valoare trebuie să conţină numai litere, numere, liniuţe de subliniere, slash-uri si dash-uri." -#: core/validators.py:72 +#: .\core\validators.py:80 +msgid "This value must contain only letters, numbers, underscores or hyphens." +msgstr "Această valoare trebuie să conţină numai litere, numere, liniuţe de subliniere şi liniute." + +#: .\core\validators.py:84 msgid "Uppercase letters are not allowed here." msgstr "Literele mari nu sînt permise aici." -#: core/validators.py:76 +#: .\core\validators.py:88 msgid "Lowercase letters are not allowed here." msgstr "Literele mici nu sînt permise aici." -#: core/validators.py:83 +#: .\core\validators.py:95 msgid "Enter only digits separated by commas." msgstr "Introduceţi numai numere separate de virgule." -#: core/validators.py:95 +#: .\core\validators.py:107 msgid "Enter valid e-mail addresses separated by commas." msgstr "Introduceţi adrese de email valide separate de virgule." -#: core/validators.py:99 +#: .\core\validators.py:111 msgid "Please enter a valid IP address." msgstr "Introduceţi vă rog o adresă IP validă." -#: core/validators.py:103 +#: .\core\validators.py:115 msgid "Empty values are not allowed here." msgstr "Valorile vide nu sînt permise aici." -#: core/validators.py:107 +#: .\core\validators.py:119 msgid "Non-numeric characters aren't allowed here." msgstr "Caracterele ne-numerice nu sînt permise aici." -#: core/validators.py:111 +#: .\core\validators.py:123 msgid "This value can't be comprised solely of digits." msgstr "Această valoare nu poate conţîne numai cifre." -#: core/validators.py:116 +#: .\core\validators.py:128 +#: .\newforms\fields.py:152 msgid "Enter a whole number." msgstr "Introduceţi un număr întreg." -#: core/validators.py:120 +#: .\core\validators.py:132 msgid "Only alphabetical characters are allowed here." msgstr "Numai caractere alfabetice sînt permise aici." -#: core/validators.py:124 +#: .\core\validators.py:147 +msgid "Year must be 1900 or later." +msgstr "Anul trebuie sa fie 1900 sau posterior." + +#: .\core\validators.py:151 +#, python-format +msgid "Invalid date: %s" +msgstr "Data invalida: %s" + +#: .\core\validators.py:156 +#: .\db\models\fields\__init__.py:527 msgid "Enter a valid date in YYYY-MM-DD format." msgstr "Introduceţi o dată validă in format: AAAA-LL-ZZ." -#: core/validators.py:128 +#: .\core\validators.py:161 msgid "Enter a valid time in HH:MM format." msgstr "Introduceţi o oră în format OO:MM." -#: core/validators.py:132 db/models/fields/__init__.py:468 +#: .\core\validators.py:165 +#: .\db\models\fields\__init__.py:604 msgid "Enter a valid date/time in YYYY-MM-DD HH:MM format." msgstr "Introduceţi o dată/oră validă în format AAAA-LL-ZZ OO:MM." -#: core/validators.py:136 +#: .\core\validators.py:170 +#: .\newforms\fields.py:403 msgid "Enter a valid e-mail address." msgstr "Introduceţi o adresă de email validă." -#: core/validators.py:148 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" -"Încărcaţi o imagine validă. Fişierul încărcat nu era o imagine sau era o " -"imagine coruptă." +#: .\core\validators.py:182 +#: .\core\validators.py:474 +#: .\newforms\fields.py:433 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "Nici un fisier nu a fost trimis. Verifica tipul fisierului." -#: core/validators.py:155 +#: .\core\validators.py:193 +#: .\newforms\fields.py:459 +msgid "Upload a valid image. The file you uploaded was either not an image or a corrupted image." +msgstr "Încărcaţi o imagine validă. Fişierul încărcat nu era o imagine sau era o imagine coruptă." + +#: .\core\validators.py:200 #, python-format msgid "The URL %s does not point to a valid image." msgstr "URL-ul %s nu pointează către o imagine validă." -#: core/validators.py:159 +#: .\core\validators.py:204 #, python-format msgid "Phone numbers must be in XXX-XXX-XXXX format. \"%s\" is invalid." -msgstr "" -"Numerele de telefon trebuie să fie in format XXX-XXX-XXXX. \"%s\" e invalid." +msgstr "Numerele de telefon trebuie să fie in format XXX-XXX-XXXX. \"%s\" e invalid." -#: core/validators.py:167 +#: .\core\validators.py:212 #, python-format msgid "The URL %s does not point to a valid QuickTime video." msgstr "URL-ul %s nu pointează către o imagine video QuickTime validă." -#: core/validators.py:171 +#: .\core\validators.py:216 msgid "A valid URL is required." msgstr "E necesar un URL valid." -#: core/validators.py:185 +#: .\core\validators.py:230 #, python-format msgid "" "Valid HTML is required. Specific errors are:\n" @@ -1710,284 +3557,705 @@ msgstr "" "E necesar cod HTML valid. Erorile specifice sînt:\n" "%s" -#: core/validators.py:192 +#: .\core\validators.py:237 #, python-format msgid "Badly formed XML: %s" msgstr "Format XML invalid: %s" -#: core/validators.py:202 +#: .\core\validators.py:254 #, python-format msgid "Invalid URL: %s" msgstr "URL invalid: %s" -#: core/validators.py:206 core/validators.py:208 +#: .\core\validators.py:259 +#: .\core\validators.py:261 #, python-format msgid "The URL %s is a broken link." msgstr "URL-ul %s e invalid." -#: core/validators.py:214 +#: .\core\validators.py:267 msgid "Enter a valid U.S. state abbreviation." msgstr "Introduceţi o abreviere validă în U.S." -#: core/validators.py:229 +#: .\core\validators.py:281 #, python-format msgid "Watch your mouth! The word %s is not allowed here." msgid_plural "Watch your mouth! The words %s are not allowed here." msgstr[0] "Îngrijiţi-vă limbajul! Cuvîntul %s nu este permis aici." msgstr[1] "Îngrijiţi-vă limbajul! Cuvintele %s nu sînt permise aici." -#: core/validators.py:236 +#: .\core\validators.py:288 #, python-format msgid "This field must match the '%s' field." msgstr "Acest camp trebuie sa fie identic cu '%s'." -#: core/validators.py:255 -#, fuzzy +#: .\core\validators.py:307 msgid "Please enter something for at least one field." -msgstr "Vă rog comletaţi ambele cîmpuri sau lăsaţi-le goale pe ambele." +msgstr "Va rog completati cel putin un camp." -#: core/validators.py:264 core/validators.py:275 +#: .\core\validators.py:316 +#: .\core\validators.py:327 msgid "Please enter both fields or leave them both empty." msgstr "Vă rog comletaţi ambele cîmpuri sau lăsaţi-le goale pe ambele." -#: core/validators.py:282 +#: .\core\validators.py:335 #, python-format msgid "This field must be given if %(field)s is %(value)s" msgstr "Acest cîmp e necesar dacă %(field)s este %(value)s" -#: core/validators.py:294 +#: .\core\validators.py:348 #, python-format msgid "This field must be given if %(field)s is not %(value)s" msgstr "Acest cîmp e necesar dacă %(field)s nu este %(value)s" -#: core/validators.py:313 +#: .\core\validators.py:367 msgid "Duplicate values are not allowed." msgstr "Valorile duplicate nu sînt permise." -#: core/validators.py:336 +#: .\core\validators.py:382 +#, python-format +msgid "This value must be between %(lower)s and %(upper)s." +msgstr "Aceasta valoare trebuie sa fie cuprinsa intre %(lower)s si %(upper)s." + +#: .\core\validators.py:384 +#, python-format +msgid "This value must be at least %s." +msgstr "Această valoare trebuie să fie cel putin %s." + +#: .\core\validators.py:386 +#, python-format +msgid "This value must be no more than %s." +msgstr "Această valoare trebuie să nu fie mai mult de %s." + +#: .\core\validators.py:427 #, python-format msgid "This value must be a power of %s." msgstr "Această valoare trebuie să fie o putere a lui %s." -#: core/validators.py:347 +#: .\core\validators.py:437 msgid "Please enter a valid decimal number." msgstr "Vă rog introduceţi un număr zecimal valid." -#: core/validators.py:349 +#: .\core\validators.py:444 #, python-format msgid "Please enter a valid decimal number with at most %s total digit." -msgid_plural "" -"Please enter a valid decimal number with at most %s total digits." +msgid_plural "Please enter a valid decimal number with at most %s total digits." msgstr[0] "Vă rog introduceţi un număr zecimal valid cu cel mult %s cifră." msgstr[1] "Vă rog introduceţi un număr zecimal valid cu cel mult %s cifre." -#: core/validators.py:352 +#: .\core\validators.py:447 +#, python-format +msgid "Please enter a valid decimal number with a whole part of at most %s digit." +msgid_plural "Please enter a valid decimal number with a whole part of at most %s digits." +msgstr[0] "Vă rog introduceţi un număr zecimal valid cu partea intreaga cel mult %s cifră." +msgstr[1] "Vă rog introduceţi un număr zecimal valid cu partea intreaga cel mult %s cifre." + +#: .\core\validators.py:450 #, python-format msgid "Please enter a valid decimal number with at most %s decimal place." -msgid_plural "" -"Please enter a valid decimal number with at most %s decimal places." +msgid_plural "Please enter a valid decimal number with at most %s decimal places." msgstr[0] "Vă rog introduceţi un număr zecimal valid cu cel mult %s zecimală." msgstr[1] "Vă rog introduceţi un număr zecimal valid cu cel mult %s zecimale." -#: core/validators.py:362 +#: .\core\validators.py:458 +msgid "Please enter a valid floating point number." +msgstr "Vă rog introduceţi un număr cu virgula valid." + +#: .\core\validators.py:467 #, python-format msgid "Make sure your uploaded file is at least %s bytes big." msgstr "Asigură-te că fişierul încărcact are cel puţin %s octeţi." -#: core/validators.py:363 +#: .\core\validators.py:468 #, python-format msgid "Make sure your uploaded file is at most %s bytes big." msgstr "Asigură-te că fişierul încărcact are cel mult %s octeţi." -#: core/validators.py:376 +#: .\core\validators.py:485 msgid "The format for this field is wrong." msgstr "Formatul acestui cîmp este invalid." -#: core/validators.py:391 +#: .\core\validators.py:500 msgid "This field is invalid." msgstr "Cîmpul este invalid." -#: core/validators.py:426 +#: .\core\validators.py:536 #, python-format msgid "Could not retrieve anything from %s." msgstr "Nu pot prelua nimic de la %s." -#: core/validators.py:429 +#: .\core\validators.py:539 #, python-format -msgid "" -"The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'." -msgstr "" -"URL-ul %(url)s a returnat un header Content-Type invalid '%(contenttype)s'." +msgid "The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'." +msgstr "URL-ul %(url)s a returnat un header Content-Type invalid '%(contenttype)s'." -#: core/validators.py:462 +#: .\core\validators.py:572 #, python-format -msgid "" -"Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with " -"\"%(start)s\".)" -msgstr "" -"Te rog închide tagurile %(tag)s din linia %(line)s. ( Linia începe cu \"%" -"(start)s\".)" +msgid "Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with \"%(start)s\".)" +msgstr "Te rog închide tagurile %(tag)s din linia %(line)s. ( Linia începe cu \"%(start)s\".)" -#: core/validators.py:466 +#: .\core\validators.py:576 #, python-format -msgid "" -"Some text starting on line %(line)s is not allowed in that context. (Line " -"starts with \"%(start)s\".)" -msgstr "" -"Textul începînd cu linia %(line)s nu e permis în acest context. (Linia " -"începînd cu \"%(start)s\".)" +msgid "Some text starting on line %(line)s is not allowed in that context. (Line starts with \"%(start)s\".)" +msgstr "Textul începînd cu linia %(line)s nu e permis în acest context. (Linia începînd cu \"%(start)s\".)" -#: core/validators.py:471 +#: .\core\validators.py:581 #, python-format -msgid "" -"\"%(attr)s\" on line %(line)s is an invalid attribute. (Line starts with \"%" -"(start)s\".)" -msgstr "" -"\"%(attr)s\" în linia %(line)s e un atribut invalid. (Linia începînd cu \"%" -"(start)s\".)" +msgid "\"%(attr)s\" on line %(line)s is an invalid attribute. (Line starts with \"%(start)s\".)" +msgstr "\"%(attr)s\" în linia %(line)s e un atribut invalid. (Linia începînd cu \"%(start)s\".)" -#: core/validators.py:476 +#: .\core\validators.py:586 #, python-format -msgid "" -"\"<%(tag)s>\" on line %(line)s is an invalid tag. (Line starts with \"%" -"(start)s\".)" -msgstr "" -"\"<%(tag)s>\" în linia %(line)s este un tag invalid. (Linia începe cu \"%" -"(start)s\"." +msgid "\"<%(tag)s>\" on line %(line)s is an invalid tag. (Line starts with \"%(start)s\".)" +msgstr "\"<%(tag)s>\" în linia %(line)s este un tag invalid. (Linia începe cu \"%(start)s\"." -#: core/validators.py:480 +#: .\core\validators.py:590 #, python-format -msgid "" -"A tag on line %(line)s is missing one or more required attributes. (Line " -"starts with \"%(start)s\".)" -msgstr "" -"Unui tag din linia %(line)s îi lipseşte unul sau mai multe atribute. (Linia " -"începe cu \"%(start)s\".)" +msgid "A tag on line %(line)s is missing one or more required attributes. (Line starts with \"%(start)s\".)" +msgstr "Unui tag din linia %(line)s îi lipseşte unul sau mai multe atribute. (Linia începe cu \"%(start)s\".)" -#: core/validators.py:485 +#: .\core\validators.py:595 #, python-format -msgid "" -"The \"%(attr)s\" attribute on line %(line)s has an invalid value. (Line " -"starts with \"%(start)s\".)" -msgstr "" -"Atributul \"%(attr)s\" din linia %(line)s are o valoare invalidă. ( Linia " -"începe cu \"%(start)s\".)" +msgid "The \"%(attr)s\" attribute on line %(line)s has an invalid value. (Line starts with \"%(start)s\".)" +msgstr "Atributul \"%(attr)s\" din linia %(line)s are o valoare invalidă. ( Linia începe cu \"%(start)s\".)" -#: db/models/manipulators.py:302 +#: .\db\models\manipulators.py:308 #, python-format msgid "%(object)s with this %(type)s already exists for the given %(field)s." -msgstr "" +msgstr "%(object)s de acest tip %(type)s exista deja pentru %(field)s dat." -#: db/models/fields/__init__.py:40 +#: .\db\models\fields\__init__.py:52 #, python-format msgid "%(optname)s with this %(fieldname)s already exists." -msgstr "" +msgstr "%(optname)s cu acest %(fieldname)s exista deja." -#: db/models/fields/__init__.py:114 db/models/fields/__init__.py:265 -#: db/models/fields/__init__.py:542 db/models/fields/__init__.py:553 -#: forms/__init__.py:346 +#: .\db\models\fields\__init__.py:161 +#: .\db\models\fields\__init__.py:327 +#: .\db\models\fields\__init__.py:759 +#: .\db\models\fields\__init__.py:770 +#: .\newforms\fields.py:46 +#: .\oldforms\__init__.py:374 msgid "This field is required." msgstr "Campul acesta trebuie completat obligatoriu." -#: db/models/fields/__init__.py:337 -#, fuzzy +#: .\db\models\fields\__init__.py:427 msgid "This value must be an integer." -msgstr "Această valoare trebuie să fie o putere a lui %s." +msgstr "Această valoare trebuie să fie un numar intreg" -#: db/models/fields/__init__.py:369 -#, fuzzy +#: .\db\models\fields\__init__.py:466 msgid "This value must be either True or False." -msgstr "Această valoare trebuie să fie o putere a lui %s." +msgstr "Această valoare trebuie să fie ori falsa ori adevarata" -#: db/models/fields/__init__.py:385 -#, fuzzy +#: .\db\models\fields\__init__.py:490 msgid "This field cannot be null." -msgstr "Cîmpul este invalid." +msgstr "Cîmpul nu poate fi gol" -#: db/models/fields/__init__.py:562 +#: .\db\models\fields\__init__.py:668 +msgid "This value must be a decimal number." +msgstr "Această valoare trebuie să fie un numar zecimal." + +#: .\db\models\fields\__init__.py:779 msgid "Enter a valid filename." msgstr "Introduceti un nume de fisier valid." -#: db/models/fields/related.py:43 +#: .\db\models\fields\__init__.py:960 +msgid "This value must be either None, True or False." +msgstr "Această valoare trebuie să fie falsa, adevarata sau nici una." + +#: .\db\models\fields\related.py:93 #, python-format msgid "Please enter a valid %s." msgstr "Introduceti va rog un %s valid." -#: db/models/fields/related.py:579 -#, fuzzy +#: .\db\models\fields\related.py:701 msgid "Separate multiple IDs with commas." msgstr "Separă ID-urile multiple cu virgulă." -#: db/models/fields/related.py:581 -#, fuzzy -msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "" -" Ţine apăsat \"Control\", sau \"Command\" pe un Mac, pentru a selecta mai " -"multe." +#: .\db\models\fields\related.py:703 +msgid "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr " Ţine apăsat \"Control\", sau \"Command\" pe un Mac, pentru a selectie multipla." -#: db/models/fields/related.py:625 +#: .\db\models\fields\related.py:750 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." -msgid_plural "" -"Please enter valid %(self)s IDs. The values %(value)r are invalid." -msgstr[0] "" -msgstr[1] "" +msgid_plural "Please enter valid %(self)s IDs. The values %(value)r are invalid." +msgstr[0] "Introduceti un ID valid pentru %(self)s. Valoarea %(value)r nu este valida." +msgstr[1] "Introduceti ID-uri valide pentru %(self)s. Valorile %(value)r nu sunt valide." -#: forms/__init__.py:380 +#: .\newforms\fields.py:47 +msgid "Enter a valid value." +msgstr "Introduceti o valoare valida." + +#: .\newforms\fields.py:124 #, python-format -msgid "Ensure your text is less than %s character." -msgid_plural "Ensure your text is less than %s characters." -msgstr[0] "" -msgstr[1] "" -"Va rugam asigurati-va ca textul dumneavoastra are mai putin de %s caractere." +msgid "Ensure this value has at most %(max)d characters (it has %(length)d)." +msgstr "Asigurati-va ca aceasta valoare are cel mult %(max)d caractere (are %(length)d)." -#: forms/__init__.py:385 -#, fuzzy -msgid "Line breaks are not allowed here." -msgstr "Literele mici nu sînt permise aici." - -#: forms/__init__.py:480 forms/__init__.py:551 forms/__init__.py:589 +#: .\newforms\fields.py:125 #, python-format -msgid "Select a valid choice; '%(data)s' is not in %(choices)s." -msgstr "" +msgid "Ensure this value has at least %(min)d characters (it has %(length)d)." +msgstr "Asigurati-va ca aceasta valoare are cel putin %(min)d caractere (are %(length)d)." -#: forms/__init__.py:645 +#: .\newforms\fields.py:153 +#: .\newforms\fields.py:182 +#: .\newforms\fields.py:211 +#, python-format +msgid "Ensure this value is less than or equal to %s." +msgstr "Asigurati-va ca aceasta valoare este mai mica sau egala cu %s." + +#: .\newforms\fields.py:154 +#: .\newforms\fields.py:183 +#: .\newforms\fields.py:212 +#, python-format +msgid "Ensure this value is greater than or equal to %s." +msgstr "Asigurati-va ca aceasta valoare este mai mare sau egala cu %s." + +#: .\newforms\fields.py:181 +#: .\newforms\fields.py:210 +msgid "Enter a number." +msgstr "Introduceţi un număr." + +#: .\newforms\fields.py:213 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Asigurati-va ca nu exista mai mult de %s cifre in total." + +#: .\newforms\fields.py:214 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Asigurati-va ca nu exista mai mult de %s spatii zecimale." + +#: .\newforms\fields.py:215 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Asigurati-va ca nu exista mai mult de %s cifre inainte de virgula." + +#: .\newforms\fields.py:263 +#: .\newforms\fields.py:751 +msgid "Enter a valid date." +msgstr "Introduceti o data valida." + +#: .\newforms\fields.py:296 +#: .\newforms\fields.py:752 +msgid "Enter a valid time." +msgstr "Introduceti o ora valida." + +#: .\newforms\fields.py:335 +msgid "Enter a valid date/time." +msgstr "Introduceti o data/ora valida." + +#: .\newforms\fields.py:434 +msgid "No file was submitted." +msgstr "Nici un fisier nu a fost trimis." + +#: .\newforms\fields.py:435 +#: .\oldforms\__init__.py:689 msgid "The submitted file is empty." msgstr "Fisierul uploadat este gol" -#: forms/__init__.py:699 -#, fuzzy +#: .\newforms\fields.py:497 +msgid "Enter a valid URL." +msgstr "Introduceti un URL valid." + +#: .\newforms\fields.py:498 +msgid "This URL appears to be a broken link." +msgstr "URL-ul e invalid." + +#: .\newforms\fields.py:560 +#: .\newforms\models.py:299 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "Selectati o optiune valida. Aceasta optiune nu face parte din optiunile disponibile." + +#: .\newforms\fields.py:599 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "Selectati o optiune valida. %(value)s nu face parte din optiunile disponibile." + +#: .\newforms\fields.py:600 +#: .\newforms\fields.py:662 +#: .\newforms\models.py:371 +msgid "Enter a list of values." +msgstr "Introduceti o lista de valori." + +#: .\newforms\fields.py:780 +msgid "Enter a valid IPv4 address." +msgstr "Introduceţi o adresă IPv4 valida." + +#: .\newforms\models.py:372 +#, python-format +msgid "Select a valid choice. %s is not one of the available choices." +msgstr "Selectati o optiune valida. %s nu face parte din optiunile disponibile." + +#: .\oldforms\__init__.py:409 +#, python-format +msgid "Ensure your text is less than %s character." +msgid_plural "Ensure your text is less than %s characters." +msgstr[0] "Asigurati-va ca textul dumneavoastra are mai putin de %s caracter." +msgstr[1] "Asigurati-va ca textul dumneavoastra are mai putin de %s caractere." + +#: .\oldforms\__init__.py:414 +msgid "Line breaks are not allowed here." +msgstr "Randurile noi nu sînt permise aici." + +#: .\oldforms\__init__.py:512 +#: .\oldforms\__init__.py:586 +#: .\oldforms\__init__.py:625 +#, python-format +msgid "Select a valid choice; '%(data)s' is not in %(choices)s." +msgstr "Selectati o optiune valida. '%(data)s' nu face parte din %(choices)s." + +#: .\oldforms\__init__.py:745 msgid "Enter a whole number between -32,768 and 32,767." -msgstr "Introduceţi un număr întreg." +msgstr "Introduceţi un număr întreg cu valoare intre -32,768 si 32,767." -#: forms/__init__.py:708 -#, fuzzy +#: .\oldforms\__init__.py:755 msgid "Enter a positive number." -msgstr "Introduceţi un număr întreg." +msgstr "Introduceţi un număr pozitiv." -#: forms/__init__.py:717 -#, fuzzy +#: .\oldforms\__init__.py:765 msgid "Enter a whole number between 0 and 32,767." -msgstr "Introduceţi un număr întreg." +msgstr "Introduceţi un număr întreg cu valoare intre 0 si 32,767." -#: template/defaultfilters.py:379 +#: .\template\defaultfilters.py:698 msgid "yes,no,maybe" msgstr "da,nu,poate" +#: .\template\defaultfilters.py:729 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" + +#: .\template\defaultfilters.py:731 +#, python-format +msgid "%.1f KB" +msgstr "%.1f KB" + +#: .\template\defaultfilters.py:733 +#, python-format +msgid "%.1f MB" +msgstr "%.1f MB" + +#: .\template\defaultfilters.py:734 +#, python-format +msgid "%.1f GB" +msgstr "%.1f GB" + +#: .\utils\dateformat.py:41 +msgid "p.m." +msgstr "p.m." + +#: .\utils\dateformat.py:42 +msgid "a.m." +msgstr "a.m." + +#: .\utils\dateformat.py:47 +msgid "PM" +msgstr "PM" + +#: .\utils\dateformat.py:48 +msgid "AM" +msgstr "AM" + +#: .\utils\dateformat.py:97 +msgid "midnight" +msgstr "miezul noptii" + +#: .\utils\dateformat.py:99 +msgid "noon" +msgstr "amiaza" + +#: .\utils\dates.py:6 +msgid "Monday" +msgstr "Luni" + +#: .\utils\dates.py:6 +msgid "Tuesday" +msgstr "Marţi" + +#: .\utils\dates.py:6 +msgid "Wednesday" +msgstr "Miercuri" + +#: .\utils\dates.py:6 +msgid "Thursday" +msgstr "Joi" + +#: .\utils\dates.py:6 +msgid "Friday" +msgstr "Vineri" + +#: .\utils\dates.py:7 +msgid "Saturday" +msgstr "Sîmbătă" + +#: .\utils\dates.py:7 +msgid "Sunday" +msgstr "Duminică" + +#: .\utils\dates.py:10 +msgid "Mon" +msgstr "Lun" + +#: .\utils\dates.py:10 +msgid "Tue" +msgstr "Mar" + +#: .\utils\dates.py:10 +msgid "Wed" +msgstr "Mie" + +#: .\utils\dates.py:10 +msgid "Thu" +msgstr "Joi" + +#: .\utils\dates.py:10 +msgid "Fri" +msgstr "Vin" + +#: .\utils\dates.py:11 +msgid "Sat" +msgstr "Sam" + +#: .\utils\dates.py:11 +msgid "Sun" +msgstr "Dum" + +#: .\utils\dates.py:18 +msgid "January" +msgstr "Ianuarie" + +#: .\utils\dates.py:18 +msgid "February" +msgstr "Februarie" + +#: .\utils\dates.py:18 +#: .\utils\dates.py:31 +msgid "March" +msgstr "Martie" + +#: .\utils\dates.py:18 +#: .\utils\dates.py:31 +msgid "April" +msgstr "Aprilie" + +#: .\utils\dates.py:18 +#: .\utils\dates.py:31 +msgid "May" +msgstr "Mai" + +#: .\utils\dates.py:18 +#: .\utils\dates.py:31 +msgid "June" +msgstr "Iunie" + +#: .\utils\dates.py:19 +#: .\utils\dates.py:31 +msgid "July" +msgstr "Iulie" + +#: .\utils\dates.py:19 +msgid "August" +msgstr "August" + +#: .\utils\dates.py:19 +msgid "September" +msgstr "Septembrie" + +#: .\utils\dates.py:19 +msgid "October" +msgstr "Octombrie" + +#: .\utils\dates.py:19 +msgid "November" +msgstr "Noiembrie" + +#: .\utils\dates.py:20 +msgid "December" +msgstr "Decembrie" + +#: .\utils\dates.py:23 +msgid "jan" +msgstr "ian" + +#: .\utils\dates.py:23 +msgid "feb" +msgstr "feb" + +#: .\utils\dates.py:23 +msgid "mar" +msgstr "mar" + +#: .\utils\dates.py:23 +msgid "apr" +msgstr "apr" + +#: .\utils\dates.py:23 +msgid "may" +msgstr "mai" + +#: .\utils\dates.py:23 +msgid "jun" +msgstr "iun" + +#: .\utils\dates.py:24 +msgid "jul" +msgstr "iul" + +#: .\utils\dates.py:24 +msgid "aug" +msgstr "aug" + +#: .\utils\dates.py:24 +msgid "sep" +msgstr "sep" + +#: .\utils\dates.py:24 +msgid "oct" +msgstr "oct" + +#: .\utils\dates.py:24 +msgid "nov" +msgstr "noi" + +#: .\utils\dates.py:24 +msgid "dec" +msgstr "dec" + +#: .\utils\dates.py:31 +msgid "Jan." +msgstr "Ian." + +#: .\utils\dates.py:31 +msgid "Feb." +msgstr "Feb." + +#: .\utils\dates.py:32 +msgid "Aug." +msgstr "Aug." + +#: .\utils\dates.py:32 +msgid "Sept." +msgstr "Sept." + +#: .\utils\dates.py:32 +msgid "Oct." +msgstr "Oct" + +#: .\utils\dates.py:32 +msgid "Nov." +msgstr "Noi." + +#: .\utils\dates.py:32 +msgid "Dec." +msgstr "Dec." + +#: .\utils\text.py:127 +msgid "or" +msgstr "sau" + +#: .\utils\timesince.py:21 +msgid "year" +msgid_plural "years" +msgstr[0] "an" +msgstr[1] "ani" + +#: .\utils\timesince.py:22 +msgid "month" +msgid_plural "months" +msgstr[0] "luna" +msgstr[1] "luni" + +#: .\utils\timesince.py:23 +msgid "week" +msgid_plural "weeks" +msgstr[0] "saptamana" +msgstr[1] "saptamani" + +#: .\utils\timesince.py:24 +msgid "day" +msgid_plural "days" +msgstr[0] "zi" +msgstr[1] "zile" + +#: .\utils\timesince.py:25 +msgid "hour" +msgid_plural "hours" +msgstr[0] "ora" +msgstr[1] "ore" + +#: .\utils\timesince.py:26 +msgid "minute" +msgid_plural "minutes" +msgstr[0] "minut" +msgstr[1] "minute" + +#: .\utils\timesince.py:46 +msgid "minutes" +msgstr "minute" + +#: .\utils\timesince.py:51 +#, python-format +msgid "%(number)d %(type)s" +msgstr "%(number)d %(type)s" + +#: .\utils\timesince.py:57 +#, python-format +msgid ", %(number)d %(type)s" +msgstr ", %(number)d %(type)s" + +#: .\utils\translation\trans_real.py:403 +msgid "DATE_FORMAT" +msgstr "j N Y" + +#: .\utils\translation\trans_real.py:404 +msgid "DATETIME_FORMAT" +msgstr "j N Y, H:i:s" + +#: .\utils\translation\trans_real.py:405 +msgid "TIME_FORMAT" +msgstr "H:i:s" + +#: .\utils\translation\trans_real.py:421 +msgid "YEAR_MONTH_FORMAT" +msgstr "F Y" + +#: .\utils\translation\trans_real.py:422 +msgid "MONTH_DAY_FORMAT" +msgstr "j F" + +#: .\views\generic\create_update.py:43 +#, python-format +msgid "The %(verbose_name)s was created successfully." +msgstr "%(verbose_name)s a fost creat cu succes." + +#: .\views\generic\create_update.py:117 +#, python-format +msgid "The %(verbose_name)s was updated successfully." +msgstr "%(verbose_name)s a fost actualizat cu succes." + +#: .\views\generic\create_update.py:184 +#, python-format +msgid "The %(verbose_name)s was deleted." +msgstr "%(verbose_name)s a fost sters." + +#~ msgid "Have you forgotten your password?" +#~ msgstr "Ai uitat parola?" +#~ msgid "Use '[algo]$[salt]$[hexdigest]'" +#~ msgstr "Foloseste '[algo]$[salt]$[hexdigest]'" + #, fuzzy #~ msgid "Comments" #~ msgstr "permite comentarii" - #~ msgid "label" #~ msgstr "etichetă" - #~ msgid "package" #~ msgstr "pachet" - #~ msgid "packages" #~ msgstr "pachete" #, fuzzy #~ msgid "count" #~ msgstr "conţinut" + diff --git a/django/conf/locale/ro/LC_MESSAGES/djangojs.mo b/django/conf/locale/ro/LC_MESSAGES/djangojs.mo index fae1fdc90e..0e594fa5d8 100644 Binary files a/django/conf/locale/ro/LC_MESSAGES/djangojs.mo and b/django/conf/locale/ro/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/ro/LC_MESSAGES/djangojs.po b/django/conf/locale/ro/LC_MESSAGES/djangojs.po index 120d08d70d..7c91fb2e45 100644 --- a/django/conf/locale/ro/LC_MESSAGES/djangojs.po +++ b/django/conf/locale/ro/LC_MESSAGES/djangojs.po @@ -1,118 +1,116 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. # -#, fuzzy + msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-06-28 17:36+1000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" +"PO-Revision-Date: 2008-04-25 21:57+0200\n" +"Last-Translator: \n" +"Language-Team: Romanian \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: contrib/admin/media/js/SelectFilter2.js:33 -#, perl-format +#, fuzzy, perl-format msgid "Available %s" -msgstr "" +msgstr "%s disponibil" #: contrib/admin/media/js/SelectFilter2.js:41 msgid "Choose all" -msgstr "" +msgstr "Alege toate" #: contrib/admin/media/js/SelectFilter2.js:46 msgid "Add" -msgstr "" +msgstr "Adauga" #: contrib/admin/media/js/SelectFilter2.js:48 msgid "Remove" -msgstr "" +msgstr "Sterge" #: contrib/admin/media/js/SelectFilter2.js:53 #, perl-format msgid "Chosen %s" -msgstr "" +msgstr "%s ales" #: contrib/admin/media/js/SelectFilter2.js:54 msgid "Select your choice(s) and click " -msgstr "" +msgstr "Selectati-va alegera(ile) si dati click " #: contrib/admin/media/js/SelectFilter2.js:59 msgid "Clear all" -msgstr "" +msgstr "Elibereaza toate" #: contrib/admin/media/js/dateparse.js:32 #: contrib/admin/media/js/calendar.js:24 -msgid "" -"January February March April May June July August September October November " -"December" -msgstr "" +msgid "January February March April May June July August September October November December" +msgstr "Ianuarie Februare Martie Aprilie Mai Iunie Iulie August Septembrie Octombrie Noiembrie Decembrie" #: contrib/admin/media/js/dateparse.js:33 msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday" -msgstr "" +msgstr "Duminica Luni Marti Miercuri Joi Vinery Sambata" #: contrib/admin/media/js/calendar.js:25 msgid "S M T W T F S" -msgstr "" +msgstr "D L M M J V S" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:47 #: contrib/admin/media/js/admin/DateTimeShortcuts.js:81 msgid "Now" -msgstr "" +msgstr "Acum" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:51 msgid "Clock" -msgstr "" +msgstr "Ceas" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:78 msgid "Choose a time" -msgstr "" +msgstr "Alege o ora" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:82 msgid "Midnight" -msgstr "" +msgstr "Miezul noptii" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:83 msgid "6 a.m." -msgstr "" +msgstr "6 a.m." #: contrib/admin/media/js/admin/DateTimeShortcuts.js:84 msgid "Noon" -msgstr "" +msgstr "Amiaza" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:88 #: contrib/admin/media/js/admin/DateTimeShortcuts.js:183 msgid "Cancel" -msgstr "" +msgstr "Anuleaza" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:128 #: contrib/admin/media/js/admin/DateTimeShortcuts.js:177 msgid "Today" -msgstr "" +msgstr "Astazi" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:132 msgid "Calendar" -msgstr "" +msgstr "Calendar" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:175 msgid "Yesterday" -msgstr "" +msgstr "Ieri" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:179 msgid "Tomorrow" -msgstr "" +msgstr "Maine" #: contrib/admin/media/js/admin/CollapsedFieldsets.js:34 #: contrib/admin/media/js/admin/CollapsedFieldsets.js:72 msgid "Show" -msgstr "" +msgstr "Arata" #: contrib/admin/media/js/admin/CollapsedFieldsets.js:63 msgid "Hide" -msgstr "" +msgstr "Ascunde" + diff --git a/django/conf/locale/ru/LC_MESSAGES/djangojs.mo b/django/conf/locale/ru/LC_MESSAGES/djangojs.mo index 2d96679d83..a57a57e2e6 100644 Binary files a/django/conf/locale/ru/LC_MESSAGES/djangojs.mo and b/django/conf/locale/ru/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/ru/LC_MESSAGES/djangojs.po b/django/conf/locale/ru/LC_MESSAGES/djangojs.po index 66be20e547..2e032c2e1d 100644 --- a/django/conf/locale/ru/LC_MESSAGES/djangojs.po +++ b/django/conf/locale/ru/LC_MESSAGES/djangojs.po @@ -74,7 +74,7 @@ msgstr "Часы" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:77 msgid "Choose a time" -msgstr "Выбирите время" +msgstr "Выберите время" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:81 msgid "Midnight" diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 55f8ae4c32..2b7a690a83 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -8,7 +8,7 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, Per from django.core.paginator import QuerySetPaginator, InvalidPage from django.shortcuts import get_object_or_404, render_to_response from django.db import models -from django.db.models.query import handle_legacy_orderlist, QuerySet +from django.db.models.query import QuerySet from django.http import Http404, HttpResponse, HttpResponseRedirect from django.utils.html import escape from django.utils.text import capfirst, get_text_list @@ -627,7 +627,7 @@ class ChangeList(object): # Perform a slight optimization: Check to see whether any filters were # given. If not, use paginator.hits to calculate the number of objects, # because we've already done paginator.hits and the value is cached. - if isinstance(self.query_set._filters, models.Q) and not self.query_set._filters.kwargs: + if not self.query_set.query.where: full_result_count = result_count else: full_result_count = self.manager.count() @@ -653,15 +653,12 @@ class ChangeList(object): def get_ordering(self): lookup_opts, params = self.lookup_opts, self.params - # For ordering, first check the "ordering" parameter in the admin options, - # then check the object's default ordering. If neither of those exist, - # order descending by ID by default. Finally, look for manually-specified - # ordering from the query string. + # For ordering, first check the "ordering" parameter in the admin + # options, then check the object's default ordering. If neither of + # those exist, order descending by ID by default. Finally, look for + # manually-specified ordering from the query string. ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] - # Normalize it to new-style ordering. - ordering = handle_legacy_orderlist(ordering) - if ordering[0].startswith('-'): order_field, order_type = ordering[0][1:], 'desc' else: @@ -753,8 +750,7 @@ class ChangeList(object): for bit in self.query.split(): or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields] other_qs = QuerySet(self.model) - if qs._select_related: - other_qs = other_qs.select_related() + other_qs.dup_select_related(qs) other_qs = other_qs.filter(reduce(operator.or_, or_queries)) qs = qs & other_qs diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 1282cc3df8..e91be70d1b 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -154,6 +154,11 @@ class GenericRelation(RelatedField, Field): def get_internal_type(self): return "ManyToManyField" + def db_type(self): + # Since we're simulating a ManyToManyField, in effect, best return the + # same db_type as well. + return None + class ReverseGenericRelatedObjectsDescriptor(object): """ This class provides the functionality that makes the related-object diff --git a/django/contrib/gis/db/backend/__init__.py b/django/contrib/gis/db/backend/__init__.py index 9b4bc20ae8..e687e85ead 100644 --- a/django/contrib/gis/db/backend/__init__.py +++ b/django/contrib/gis/db/backend/__init__.py @@ -4,27 +4,20 @@ Specifically, this module will import the correct routines and modules needed for GeoDjango. - (1) GeoBackEndField, a base class needed for GeometryField. - (2) GIS_TERMS, a list of acceptable geographic lookup types for - the backend. - (3) The `parse_lookup` function, used for spatial SQL construction by - the GeoQuerySet. - (4) The `create_spatial_db`, and `get_geo_where_clause` - (needed by `parse_lookup`) functions. - (5) The `SpatialBackend` object, which contains information specific - to the spatial backend. + (1) `GeoBackEndField`, a base class needed for GeometryField. + (2) `GeoWhereNode`, a subclass of `WhereNode` used to contruct spatial SQL. + (3) `SpatialBackend`, a container object for information specific to the + spatial backend. """ from django.conf import settings -from django.db import connection -from django.db.models.query import field_choices, find_field, get_where_clause, \ - FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS -from django.utils.datastructures import SortedDict +from django.db.models.sql.query import QUERY_TERMS +from django.db.models.sql.where import WhereNode from django.contrib.gis.db.backend.util import gqn # These routines (needed by GeoManager), default to False. ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8)) -# Lookup types in which the rest of the parameters are not +# Lookup types in which the rest of the parameters are not # needed to be substitute in the WHERE SQL (e.g., the 'relate' # operation on Oracle does not need the mask substituted back # into the query SQL.). @@ -68,6 +61,28 @@ elif settings.DATABASE_ENGINE == 'mysql': else: raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE) +class GeoWhereNode(WhereNode): + """ + The GeoWhereNode calls the `get_geo_where_clause` from the appropriate + spatial backend in order to construct correct spatial SQL. + """ + def make_atom(self, child, qn): + table_alias, name, field, lookup_type, value = child + if hasattr(field, '_geom'): + if lookup_type in GIS_TERMS: + # Getting the geographic where clause; substitution parameters + # will be populated in the GeoFieldSQL object returned by the + # GeometryField. + gwc = get_geo_where_clause(lookup_type, table_alias, field, value) + where, params = field.get_db_prep_lookup(lookup_type, value) + return gwc % tuple(where), params + else: + raise TypeError('Invalid lookup type: %r' % lookup_type) + else: + # If not a GeometryField, call the `make_atom` from the + # base class. + return super(GeoWhereNode, self).make_atom(child, qn) + class SpatialBackend(object): "A container for properties of the SpatialBackend." # Stored procedure names used by the `GeoManager`. @@ -96,246 +111,3 @@ class SpatialBackend(object): # Adaptor class used for quoting GEOS geometries in the database. Adaptor = GeoAdaptor - -#### query.py overloaded functions #### -# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py -# counterparts to support constructing SQL for geographic queries. -# -# Status: Synced with r7098. -# -def parse_lookup(kwarg_items, opts): - # Helper function that handles converting API kwargs - # (e.g. "name__exact": "tom") to SQL. - # Returns a tuple of (joins, where, params). - - # 'joins' is a sorted dictionary describing the tables that must be joined - # to complete the query. The dictionary is sorted because creation order - # is significant; it is a dictionary to ensure uniqueness of alias names. - # - # Each key-value pair follows the form - # alias: (table, join_type, condition) - # where - # alias is the AS alias for the joined table - # table is the actual table name to be joined - # join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc) - # condition is the where-like statement over which narrows the join. - # alias will be derived from the lookup list name. - # - # At present, this method only every returns INNER JOINs; the option is - # there for others to implement custom Q()s, etc that return other join - # types. - joins, where, params = SortedDict(), [], [] - - for kwarg, value in kwarg_items: - path = kwarg.split(LOOKUP_SEPARATOR) - # Extract the last elements of the kwarg. - # The very-last is the lookup_type (equals, like, etc). - # The second-last is the table column on which the lookup_type is - # to be performed. If this name is 'pk', it will be substituted with - # the name of the primary key. - # If there is only one part, or the last part is not a query - # term, assume that the query is an __exact - lookup_type = path.pop() - if lookup_type == 'pk': - lookup_type = 'exact' - path.append(None) - elif len(path) == 0 or not ((lookup_type in QUERY_TERMS) or (lookup_type in GIS_TERMS)): - path.append(lookup_type) - lookup_type = 'exact' - - if len(path) < 1: - raise TypeError, "Cannot parse keyword query %r" % kwarg - - if value is None: - # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject - # all uses of None as a query value. - if lookup_type != 'exact': - raise ValueError, "Cannot use None as a query value" - elif callable(value): - value = value() - - joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - return joins, where, params - -def lookup_inner(path, lookup_type, value, opts, table, column): - qn = connection.ops.quote_name - joins, where, params = SortedDict(), [], [] - current_opts = opts - current_table = table - current_column = column - intermediate_table = None - join_required = False - - name = path.pop(0) - # Has the primary key been requested? If so, expand it out - # to be the name of the current class' primary key - if name is None or name == 'pk': - name = current_opts.pk.name - - # Try to find the name in the fields associated with the current class - try: - # Does the name belong to a defined many-to-many field? - field = find_field(name, current_opts.many_to_many, False) - if field: - new_table = current_table + '__' + name - new_opts = field.rel.to._meta - new_column = new_opts.pk.column - - # Need to create an intermediate table join over the m2m table - # This process hijacks current_table/column to point to the - # intermediate table. - current_table = "m2m_" + new_table - intermediate_table = field.m2m_db_table() - join_column = field.m2m_reverse_name() - intermediate_column = field.m2m_column_name() - - raise FieldFound - - # Does the name belong to a reverse defined many-to-many field? - field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True) - if field: - new_table = current_table + '__' + name - new_opts = field.opts - new_column = new_opts.pk.column - - # Need to create an intermediate table join over the m2m table. - # This process hijacks current_table/column to point to the - # intermediate table. - current_table = "m2m_" + new_table - intermediate_table = field.field.m2m_db_table() - join_column = field.field.m2m_column_name() - intermediate_column = field.field.m2m_reverse_name() - - raise FieldFound - - # Does the name belong to a one-to-many field? - field = find_field(name, current_opts.get_all_related_objects(), True) - if field: - new_table = table + '__' + name - new_opts = field.opts - new_column = field.field.column - join_column = opts.pk.column - - # 1-N fields MUST be joined, regardless of any other conditions. - join_required = True - - raise FieldFound - - # Does the name belong to a one-to-one, many-to-one, or regular field? - field = find_field(name, current_opts.fields, False) - if field: - if field.rel: # One-to-One/Many-to-one field - new_table = current_table + '__' + name - new_opts = field.rel.to._meta - new_column = new_opts.pk.column - join_column = field.column - raise FieldFound - elif path: - # For regular fields, if there are still items on the path, - # an error has been made. We munge "name" so that the error - # properly identifies the cause of the problem. - name += LOOKUP_SEPARATOR + path[0] - else: - raise FieldFound - - except FieldFound: # Match found, loop has been shortcut. - pass - else: # No match found. - choices = field_choices(current_opts.many_to_many, False) + \ - field_choices(current_opts.get_all_related_many_to_many_objects(), True) + \ - field_choices(current_opts.get_all_related_objects(), True) + \ - field_choices(current_opts.fields, False) - raise TypeError, "Cannot resolve keyword '%s' into field. Choices are: %s" % (name, ", ".join(choices)) - - # Check whether an intermediate join is required between current_table - # and new_table. - if intermediate_table: - joins[qn(current_table)] = ( - qn(intermediate_table), "LEFT OUTER JOIN", - "%s.%s = %s.%s" % (qn(table), qn(current_opts.pk.column), qn(current_table), qn(intermediate_column)) - ) - - if path: - # There are elements left in the path. More joins are required. - if len(path) == 1 and path[0] in (new_opts.pk.name, None) \ - and lookup_type in ('exact', 'isnull') and not join_required: - # If the next and final name query is for a primary key, - # and the search is for isnull/exact, then the current - # (for N-1) or intermediate (for N-N) table can be used - # for the search. No need to join an extra table just - # to check the primary key. - new_table = current_table - else: - # There are 1 or more name queries pending, and we have ruled out - # any shortcuts; therefore, a join is required. - joins[qn(new_table)] = ( - qn(new_opts.db_table), "INNER JOIN", - "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) - ) - # If we have made the join, we don't need to tell subsequent - # recursive calls about the column name we joined on. - join_column = None - - # There are name queries remaining. Recurse deeper. - joins2, where2, params2 = lookup_inner(path, lookup_type, value, new_opts, new_table, join_column) - - joins.update(joins2) - where.extend(where2) - params.extend(params2) - else: - # No elements left in path. Current element is the element on which - # the search is being performed. - db_type = None - - if join_required: - # Last query term is a RelatedObject - if field.field.rel.multiple: - # RelatedObject is from a 1-N relation. - # Join is required; query operates on joined table. - column = new_opts.pk.name - joins[qn(new_table)] = ( - qn(new_opts.db_table), "INNER JOIN", - "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) - ) - current_table = new_table - else: - # RelatedObject is from a 1-1 relation, - # No need to join; get the pk value from the related object, - # and compare using that. - column = current_opts.pk.name - elif intermediate_table: - # Last query term is a related object from an N-N relation. - # Join from intermediate table is sufficient. - column = join_column - elif name == current_opts.pk.name and lookup_type in ('exact', 'isnull') and current_column: - # Last query term is for a primary key. If previous iterations - # introduced a current/intermediate table that can be used to - # optimize the query, then use that table and column name. - column = current_column - else: - # Last query term was a normal field. - column = field.column - db_type = field.db_type() - - # If the field is a geometry field, then the WHERE clause will need to be obtained - # with the get_geo_where_clause() - if hasattr(field, '_geom'): - # Getting additional SQL WHERE and params arrays associated with - # the geographic field. - geo_where, geo_params = field.get_db_prep_lookup(lookup_type, value) - - # Getting the geographic WHERE clause. - gwc = get_geo_where_clause(lookup_type, current_table, field, value) - - # Appending the geographic WHERE componnents and parameters onto - # the where and params arrays. - where.append(gwc % tuple(geo_where)) - params.extend(geo_params) - else: - where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type)) - params.extend(field.get_db_prep_lookup(lookup_type, value)) - - return joins, where, params diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index 264ba40509..f36045cf02 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -1,5 +1,3 @@ -from decimal import Decimal -from django.conf import settings from django.db import connection # Getting the SpatialBackend container and the geographic quoting method. from django.contrib.gis.db.backend import SpatialBackend, gqn diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 6550f0b6e4..3c2a533ba4 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,252 +1,161 @@ -import operator from django.core.exceptions import ImproperlyConfigured from django.db import connection -from django.db.models.query import EmptyResultSet, Q, QuerySet, handle_legacy_orderlist, quote_only_if_word, orderfield2column, fill_table_cache +from django.db.models.query import sql, QuerySet, Q from django.db.models.fields import FieldDoesNotExist -from django.utils.datastructures import SortedDict +from django.contrib.gis.db.backend import gqn, GeoWhereNode, SpatialBackend, QUERY_TERMS from django.contrib.gis.db.models.fields import GeometryField, PointField -# parse_lookup depends on the spatial database backend. -from django.contrib.gis.db.backend import gqn, parse_lookup, SpatialBackend from django.contrib.gis.geos import GEOSGeometry, Point -# Shortcut booleans for determining the backend. -oracle = SpatialBackend.name == 'oracle' +# Aliases. +qn = connection.ops.quote_name +oracle = SpatialBackend.name == 'oracle' postgis = SpatialBackend.name == 'postgis' -class GeoQ(Q): - "Geographical query encapsulation object." +# All valid lookup terms. +ALL_TERMS = QUERY_TERMS.copy() +ALL_TERMS.update(dict((term, None) for term in SpatialBackend.gis_terms)) - def get_sql(self, opts): - "Overloaded to use our own parse_lookup() function." - return parse_lookup(self.kwargs.items(), opts) +# For backwards-compatibility; Q object should work just fine +# using queryset-refactor. +class GeoQ(Q): pass -class GeoQuerySet(QuerySet): - "Geographical-enabled QuerySet object." - - #### Overloaded QuerySet Routines #### - def __init__(self, model=None): - super(GeoQuerySet, self).__init__(model=model) +class GeomSQL(object): + "Simple wrapper object for geometric SQL." + def __init__(self, geo_sql): + self.sql = geo_sql + + def as_sql(self, *args, **kwargs): + return self.sql - # We only want to use the GeoQ object for our queries - self._filters = GeoQ() +# Getting the `Query` base class from the backend (needed specifically +# for Oracle backends). +Query = QuerySet().query.__class__ - # For replacement fields in the SELECT. - self._custom_select = {} - self._ewkt = None +class GeoQuery(Query): + "The Geographic Query, needed to construct spatial SQL." - # If GEOM_SELECT is defined in the backend, then it will be used - # for the selection format of the geometry column. - if SpatialBackend.select: - # Transformed geometries in Oracle use EWKT so that the SRID - # on the transformed lazy geometries is set correctly). - self._geo_fmt = SpatialBackend.select + # Overridding the valid query terms. + query_terms = ALL_TERMS + + #### Methods overridden from the base Query class #### + def __init__(self, model, conn): + super(GeoQuery, self).__init__(model, conn, where=GeoWhereNode) + # The following attributes are customized for the GeoQuerySet. + # The GeoWhereNode and SpatialBackend classes contain backend-specific + # routines and functions. + self.custom_select = {} + self.ewkt = None + + def clone(self, *args, **kwargs): + obj = super(GeoQuery, self).clone(*args, **kwargs) + # Customized selection dictionary and EWKT flag have to be added to obj. + obj.custom_select = self.custom_select.copy() + obj.ewkt = self.ewkt + return obj + + def get_default_columns(self, with_aliases=False, col_aliases=None): + """ + Computes the default columns for selecting every field in the base + model. + + Returns a list of strings, quoted appropriately for use in SQL + directly, as well as a set of aliases used in the select statement. + + This routine is overridden from Query to handle customized selection of + geometry columns. + """ + result = [] + table_alias = self.tables[0] + root_pk = self.model._meta.pk.column + seen = {None: table_alias} + qn = self.quote_name_unless_alias + qn2 = self.connection.ops.quote_name + aliases = set() + for field, model in self.model._meta.get_fields_with_model(): + try: + alias = seen[model] + except KeyError: + alias = self.join((table_alias, model._meta.db_table, + root_pk, model._meta.pk.column)) + seen[model] = alias + + # This part of the function is customized for GeoQuerySet. We + # see if there was any custom selection specified in the + # dictionary, and set up the selection format appropriately. + sel_fmt = self.get_select_format(field) + if field.column in self.custom_select: + field_sel = sel_fmt % self.custom_select[field.column] + else: + field_sel = sel_fmt % self._field_column(field, alias) + + if with_aliases and field.column in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (field_sel, c_alias)) + col_aliases.add(c_alias) + aliases.add(c_alias) + else: + r = field_sel + result.append(r) + aliases.add(r) + if with_aliases: + col_aliases.add(field.column) + return result, aliases + + #### Routines unique to GeoQuery #### + def get_select_format(self, fld): + """ + Returns the selection format string, depending on the requirements + of the spatial backend. For example, Oracle and MySQL require custom + selection formats in order to retrieve geometries in OGC WKT. For all + other fields a simple '%s' format string is returned. + """ + if SpatialBackend.select and hasattr(fld, '_geom'): + # This allows operations to be done on fields in the SELECT, + # overriding their values -- used by the Oracle and MySQL + # spatial backends to get database values as WKT, and by the + # `transform` method. + sel_fmt = SpatialBackend.select + + # Because WKT doesn't contain spatial reference information, + # the SRID is prefixed to the returned WKT to ensure that the + # transformed geometries have an SRID different than that of the + # field -- this is only used by `transform` for Oracle backends. + if self.ewkt and oracle: + sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt) else: - self._geo_fmt = '%s' + sel_fmt = '%s' + return sel_fmt - def _filter_or_exclude(self, mapper, *args, **kwargs): - # mapper is a callable used to transform Q objects, - # or None for identity transform - if mapper is None: - mapper = lambda x: x - if len(args) > 0 or len(kwargs) > 0: - assert self._limit is None and self._offset is None, \ - "Cannot filter a query once a slice has been taken." - - clone = self._clone() - if len(kwargs) > 0: - # Using the GeoQ object for our filters instead - clone._filters = clone._filters & mapper(GeoQ(**kwargs)) - if len(args) > 0: - clone._filters = clone._filters & reduce(operator.and_, map(mapper, args)) - return clone - - def _get_sql_clause(self, get_full_query=False): - qn = connection.ops.quote_name - opts = self.model._meta - - # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. - select = [] - - # This is the only component of this routine that is customized for the - # GeoQuerySet. Specifically, this allows operations to be done on fields - # in the SELECT, overriding their values -- this is different from using - # QuerySet.extra(select=foo) because extra() adds an an _additional_ - # field to be selected. Used in returning transformed geometries, and - # handling the selection of native database geometry formats. - for f in opts.fields: - # Getting the selection format string. - if hasattr(f, '_geom'): - sel_fmt = self._geo_fmt - - # If an SRID needs to specified other than what is in the field - # (like when `transform` is called), make sure to explicitly set - # the SRID by returning EWKT. - if self._ewkt and oracle: - sel_fmt = "'SRID=%d;'||%s" % (self._ewkt, sel_fmt) - else: - sel_fmt = '%s' - - # Getting the field selection substitution string - if f.column in self._custom_select: - fld_sel = self._custom_select[f.column] - else: - fld_sel = self._field_column(f) - - # Appending the selection - select.append(sel_fmt % fld_sel) - - tables = [quote_only_if_word(t) for t in self._tables] - joins = SortedDict() - where = self._where[:] - params = self._params[:] - - # Convert self._filters into SQL. - joins2, where2, params2 = self._filters.get_sql(opts) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - - # Add additional tables and WHERE clauses based on select_related. - if self._select_related: - fill_table_cache(opts, select, tables, where, - old_prefix=opts.db_table, - cache_tables_seen=[opts.db_table], - max_depth=self._max_related_depth) - - # Add any additional SELECTs. - if self._select: - select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), qn(s[0])) for s in self._select.items()]) - - # Start composing the body of the SQL statement. - sql = [" FROM", qn(opts.db_table)] - - # Compose the join dictionary into SQL describing the joins. - if joins: - sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition) - for (alias, (table, join_type, condition)) in joins.items()])) - - # Compose the tables clause into SQL. - if tables: - sql.append(", " + ", ".join(tables)) - - # Compose the where clause into SQL. - if where: - sql.append(where and "WHERE " + " AND ".join(where)) - - # ORDER BY clause - order_by = [] - if self._order_by is not None: - ordering_to_use = self._order_by - else: - ordering_to_use = opts.ordering - for f in handle_legacy_orderlist(ordering_to_use): - if f == '?': # Special case. - order_by.append(connection.ops.random_function_sql()) - else: - if f.startswith('-'): - col_name = f[1:] - order = "DESC" - else: - col_name = f - order = "ASC" - if "." in col_name: - table_prefix, col_name = col_name.split('.', 1) - table_prefix = qn(table_prefix) + '.' - else: - # Use the database table as a column prefix if it wasn't given, - # and if the requested column isn't a custom SELECT. - if "." not in col_name and col_name not in (self._select or ()): - table_prefix = qn(opts.db_table) + '.' - else: - table_prefix = '' - order_by.append('%s%s %s' % (table_prefix, qn(orderfield2column(col_name, opts)), order)) - if order_by: - sql.append("ORDER BY " + ", ".join(order_by)) - - # LIMIT and OFFSET clauses - if not oracle: - if self._limit is not None: - sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset)) - else: - assert self._offset is None, "'offset' is not allowed without 'limit'" - - return select, " ".join(sql), params - else: - # To support limits and offsets, Oracle requires some funky rewriting of an otherwise normal looking query. - select_clause = ",".join(select) - distinct = (self._distinct and "DISTINCT " or "") - - if order_by: - order_by_clause = " OVER (ORDER BY %s )" % (", ".join(order_by)) - else: - #Oracle's row_number() function always requires an order-by clause. - #So we need to define a default order-by, since none was provided. - order_by_clause = " OVER (ORDER BY %s.%s)" % \ - (qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column)) - # limit_and_offset_clause - if self._limit is None: - assert self._offset is None, "'offset' is not allowed without 'limit'" - - if self._offset is not None: - offset = int(self._offset) - else: - offset = 0 - if self._limit is not None: - limit = int(self._limit) - else: - limit = None - - limit_and_offset_clause = '' - if limit is not None: - limit_and_offset_clause = "WHERE rn > %s AND rn <= %s" % (offset, limit+offset) - elif offset: - limit_and_offset_clause = "WHERE rn > %s" % (offset) - - if len(limit_and_offset_clause) > 0: - fmt = \ - """SELECT * FROM - (SELECT %s%s, - ROW_NUMBER()%s AS rn - %s) - %s""" - full_query = fmt % (distinct, select_clause, - order_by_clause, ' '.join(sql).strip(), - limit_and_offset_clause) - else: - full_query = None - - if get_full_query: - return select, " ".join(sql), params, full_query - else: - return select, " ".join(sql), params - - def _clone(self, klass=None, **kwargs): - c = super(GeoQuerySet, self)._clone(klass, **kwargs) - c._custom_select = self._custom_select - c._ewkt = self._ewkt - return c - - #### Methods specific to the GeoQuerySet #### - def _field_column(self, field): - "Helper function that returns the database column for the given field." - qn = connection.ops.quote_name - return "%s.%s" % (qn(self.model._meta.db_table), - qn(field.column)) + def _field_column(self, field, table_alias=None): + """ + Helper function that returns the database column for the given field. + The table and column are returned (quoted) in the proper format, e.g., + `"geoapp_city"."point"`. + """ + if table_alias is None: table_alias = self.model._meta.db_table + return "%s.%s" % (self.quote_name_unless_alias(table_alias), qn(field.column)) def _geo_field(self, field_name=None): """ - Returns the first Geometry field encountered; or specified via the + Returns the first Geometry field encountered; or specified via the `field_name` keyword. """ for field in self.model._meta.fields: - if isinstance(field, GeometryField): + if isinstance(field, GeometryField): fname = field.name if field_name: if field_name == field.name: return field else: return field return False - + +class GeoQuerySet(QuerySet): + "The Geographic QuerySet." + + def __init__(self, model=None, query=None): + super(GeoQuerySet, self).__init__(model=model, query=query) + self.query = query or GeoQuery(self.model, connection) + def distance(self, *args, **kwargs): """ Returns the distance from the given geographic field name to the @@ -257,7 +166,7 @@ class GeoQuerySet(QuerySet): if not DISTANCE: raise ImproperlyConfigured('Distance() stored proecedure not available.') - # Getting the geometry field and GEOSGeometry object to base distance + # Getting the geometry field and GEOSGeometry object to base distance # calculations from. nargs = len(args) if nargs == 1: @@ -268,14 +177,14 @@ class GeoQuerySet(QuerySet): else: raise ValueError('Maximum two arguments allowed for `distance` aggregate.') - # Getting the quoted column. - geo_field = self._geo_field(field_name) + # Getting the GeometryField and quoted column. + geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('Distance output only available on GeometryFields.') - geo_col = self._field_column(geo_field) + geo_col = self.query._field_column(geo_field) # Using the field's get_db_prep_lookup() to get any needed - # transformation and distance SQL -- we pass in a 'dummy' + # transformation SQL -- we pass in a 'dummy' `contains` # `distance_lte` lookup type. where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0)) if oracle: @@ -299,7 +208,7 @@ class GeoQuerySet(QuerySet): else: dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, where[0]) dist_select = {'distance' : dist_sql} - return self.extra(select=dist_select, params=params) + return self.extra(select=dist_select, select_params=params) def extent(self, field_name=None): """ @@ -310,24 +219,25 @@ class GeoQuerySet(QuerySet): if not EXTENT: raise ImproperlyConfigured('Extent stored procedure not available.') - geo_field = self._geo_field(field_name) + # Getting the GeometryField and quoted column. + geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('Extent information only available on GeometryFields.') - geo_col = self._field_column(geo_field) + geo_col = self.query._field_column(geo_field) - # Getting the SQL for the query. - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - return None - # Constructing the query that will select the extent. - extent_sql = ('SELECT %s(%s)' % (EXTENT, geo_col)) + sql + extent_sql = '%s(%s)' % (EXTENT, geo_col) + + self.query.select = [GeomSQL(extent_sql)] + try: + esql, params = self.query.as_sql() + except sql.datastructures.EmptyResultSet: + return None # Getting a cursor, executing the query, and extracting the returned # value from the extent function. cursor = connection.cursor() - cursor.execute(extent_sql, params) + cursor.execute(esql, params) box = cursor.fetchone()[0] if box: @@ -350,18 +260,17 @@ class GeoQuerySet(QuerySet): ASGML = SpatialBackend.as_gml if not ASGML: raise ImproperlyConfigured('AsGML() stored procedure not available.') - - # If no field name explicitly given, get the first GeometryField from - # the model. - geo_field = self._geo_field(field_name) + + # Getting the GeometryField and quoted column. + geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('GML output only available on GeometryFields.') - geo_col = self._field_column(geo_field) + geo_col = self.query._field_column(geo_field) if oracle: gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)} elif postgis: - # PostGIS AsGML() aggregate function parameter order depends on the + # PostGIS AsGML() aggregate function parameter order depends on the # version -- uggh. major, minor1, minor2 = SpatialBackend.version if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): @@ -374,7 +283,7 @@ class GeoQuerySet(QuerySet): def kml(self, field_name=None, precision=8): """ - Returns KML representation of the given field name in a `kml` + Returns KML representation of the given field name in a `kml` attribute on each element of the GeoQuerySet. """ # Is KML output supported? @@ -382,20 +291,21 @@ class GeoQuerySet(QuerySet): if not ASKML: raise ImproperlyConfigured('AsKML() stored procedure not available.') - # Getting the geographic field. - geo_field = self._geo_field(field_name) + # Getting the GeometryField and quoted column. + geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('KML output only available on GeometryFields.') - geo_col = self._field_column(geo_field) + geo_col = self.query._field_column(geo_field) # Adding the AsKML function call to SELECT part of the SQL. return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)}) - + def transform(self, field_name=None, srid=4326): """ Transforms the given geometry field to the given SRID. If no SRID is provided, the transformation will default to using 4326 (WGS84). """ + # Getting the geographic field. TRANSFORM = SpatialBackend.transform if not TRANSFORM: raise ImproperlyConfigured('Transform stored procedure not available.') @@ -405,27 +315,27 @@ class GeoQuerySet(QuerySet): if isinstance(field_name, (int, long)): srid = field_name field_name = None - - # Getting the geographic field. - geo_field = self._geo_field(field_name) + + # Getting the GeometryField and quoted column. + geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('%s() only available for GeometryFields' % TRANSFORM) - + # Why cascading substitutions? Because spatial backends like # Oracle and MySQL already require a function call to convert to text, thus # when there's also a transformation we need to cascade the substitutions. # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' - geo_col = self._custom_select.get(geo_field.column, self._field_column(geo_field)) - - # Setting the key for the field's column with the custom SELECT SQL to + geo_col = self.query.custom_select.get(geo_field.column, self.query._field_column(geo_field)) + + # Setting the key for the field's column with the custom SELECT SQL to # override the geometry column returned from the database. if oracle: custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid) - self._ewkt = srid + self.query.ewkt = srid else: custom_sel = '(%s(%s, %s)) AS %s' % \ - (TRANSFORM, geo_col, srid, connection.ops.quote_name(geo_field.column)) - self._custom_select[geo_field.column] = custom_sel + (TRANSFORM, geo_col, srid, qn(geo_field.column)) + self.query.custom_select[geo_field.column] = custom_sel return self._clone() def union(self, field_name=None, tolerance=0.0005): @@ -439,31 +349,30 @@ class GeoQuerySet(QuerySet): if not UNION: raise ImproperlyConfigured('Union stored procedure not available.') - # Getting the geographic field column - geo_field = self._geo_field(field_name) + # Getting the GeometryField and quoted column. + geo_field = self.query._geo_field(field_name) if not geo_field: raise TypeError('Aggregate Union only available on GeometryFields.') - geo_col = self._field_column(geo_field) - - # Getting the SQL for the query. - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - return None + geo_col = self.query._field_column(geo_field) # Replacing the select with a call to the ST_Union stored procedure # on the geographic field column. if oracle: - union_sql = 'SELECT %s' % self._geo_fmt + union_sql = '%s' % SpatialBackend.select union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance)) - union_sql += sql else: - union_sql = ('SELECT %s(%s)' % (UNION, geo_col)) + sql + union_sql = '%s(%s)' % (UNION, geo_col) + + # Only want the union SQL to be selected. + self.query.select = [GeomSQL(union_sql)] + try: + usql, params = self.query.as_sql() + except sql.datastructures.EmptyResultSet: + return None # Getting a cursor, executing the query. cursor = connection.cursor() - cursor.execute(union_sql, params) - + cursor.execute(usql, params) if oracle: # On Oracle have to read out WKT from CLOB first. clob = cursor.fetchone()[0] @@ -471,6 +380,6 @@ class GeoQuerySet(QuerySet): else: u = None else: u = cursor.fetchone()[0] - + if u: return GEOSGeometry(u) else: return None diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index be4eb2ac7f..b06ec2130b 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -187,6 +187,7 @@ class GeoModelTest(unittest.TestCase): @no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!) def test05_extent(self): "Testing the extent() GeoManager method." + if DISABLE: return # Reference query: # `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');` # => BOX(-96.8016128540039 29.7633724212646,-95.3631439208984 32.7820587158203) @@ -378,7 +379,7 @@ class GeoModelTest(unittest.TestCase): # a ValueError. bad_args = [((pnt1, 0), TypeError), ((pnt2, 'T*T***FF*', 0), ValueError), - ((23, 'foo'), TypeError), + ((23, 'foo'), ValueError), ] for args, e in bad_args: qs = Country.objects.filter(mpoly__relate=args) diff --git a/django/contrib/gis/tests/geoapp/tests_mysql.py b/django/contrib/gis/tests/geoapp/tests_mysql.py index da6e5a3d0f..89496e81b9 100644 --- a/django/contrib/gis/tests/geoapp/tests_mysql.py +++ b/django/contrib/gis/tests/geoapp/tests_mysql.py @@ -60,7 +60,7 @@ class GeoModelTest(unittest.TestCase): inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40)) # Creating a State object using a built Polygon - ply = Polygon(shell.clone(), inner.clone()) + ply = Polygon(shell, inner) nullstate = State(name='NullState', poly=ply) self.assertEqual(4326, nullstate.poly.srid) # SRID auto-set from None nullstate.save() @@ -77,12 +77,12 @@ class GeoModelTest(unittest.TestCase): # Changing the interior ring on the poly attribute. new_inner = LinearRing((30, 30), (30, 70), (70, 70), (70, 30), (30, 30)) - nullstate.poly[1] = new_inner.clone() + ns.poly[1] = new_inner ply[1] = new_inner - self.assertEqual(4326, nullstate.poly.srid) - nullstate.save() + self.assertEqual(4326, ns.poly.srid) + ns.save() self.assertEqual(ply, State.objects.get(name='NullState').poly) - nullstate.delete() + ns.delete() def test03_contains_contained(self): "Testing the 'contained', 'contains', and 'bbcontains' lookup types." @@ -121,7 +121,19 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(1, len(qs)) self.assertEqual('Texas', qs[0].name) - def test04_equals(self): + def test04_disjoint(self): + "Testing the `disjoint` lookup type." + ptown = City.objects.get(name='Pueblo') + qs1 = City.objects.filter(point__disjoint=ptown.point) + self.assertEqual(7, qs1.count()) + # TODO: This query should work in MySQL, but it appears the + # `MBRDisjoint` function doesn't work properly (I went down + # to the SQL level for debugging and still got bogus answers). + #qs2 = State.objects.filter(poly__disjoint=ptown.point) + #self.assertEqual(1, qs2.count()) + #self.assertEqual('Kansas', qs2[0].name) + + def test05_equals(self): "Testing the 'same_as' and 'equals' lookup types." pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326) c1 = City.objects.get(point=pnt) @@ -129,7 +141,7 @@ class GeoModelTest(unittest.TestCase): c3 = City.objects.get(point__equals=pnt) for c in [c1, c2, c3]: self.assertEqual('Houston', c.name) - def test05_geometryfield(self): + def test06_geometryfield(self): "Testing GeometryField." f1 = Feature(name='Point', geom=Point(1, 1)) f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))) @@ -155,7 +167,7 @@ class GeoModelTest(unittest.TestCase): self.assertEqual(True, isinstance(f_4.geom, GeometryCollection)) self.assertEqual(f_3.geom, f_4.geom[2]) - def test06_mysql_limitations(self): + def test07_mysql_limitations(self): "Testing that union(), kml(), gml() raise exceptions." self.assertRaises(ImproperlyConfigured, City.objects.union, 'point') self.assertRaises(ImproperlyConfigured, State.objects.all().kml, 'poly') diff --git a/django/core/exceptions.py b/django/core/exceptions.py index d9fc326cf2..e5df8caca8 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -27,3 +27,8 @@ class MiddlewareNotUsed(Exception): class ImproperlyConfigured(Exception): "Django is somehow improperly configured" pass + +class FieldError(Exception): + """Some kind of problem with a model field.""" + pass + diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 4a5ec3ccde..42119915af 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -26,7 +26,7 @@ def django_table_list(only_existing=False): for app in models.get_apps(): for model in models.get_models(app): tables.append(model._meta.db_table) - tables.extend([f.m2m_db_table() for f in model._meta.many_to_many]) + tables.extend([f.m2m_db_table() for f in model._meta.local_many_to_many]) if only_existing: existing = table_list() tables = [t for t in tables if t in existing] @@ -54,12 +54,12 @@ def sequence_list(): for app in apps: for model in models.get_models(app): - for f in model._meta.fields: + for f in model._meta.local_fields: if isinstance(f, models.AutoField): sequence_list.append({'table': model._meta.db_table, 'column': f.column}) break # Only one AutoField is allowed per model, so don't bother continuing. - for f in model._meta.many_to_many: + for f in model._meta.local_many_to_many: sequence_list.append({'table': f.m2m_db_table(), 'column': None}) return sequence_list @@ -149,7 +149,7 @@ def sql_delete(app, style): if cursor and table_name_converter(model._meta.db_table) in table_names: # The table exists, so it needs to be dropped opts = model._meta - for f in opts.fields: + for f in opts.local_fields: if f.rel and f.rel.to not in to_delete: references_to_delete.setdefault(f.rel.to, []).append( (model, f) ) @@ -181,7 +181,7 @@ def sql_delete(app, style): # Output DROP TABLE statements for many-to-many tables. for model in app_models: opts = model._meta - for f in opts.many_to_many: + for f in opts.local_many_to_many: if isinstance(f.rel, generic.GenericRel): continue if cursor and table_name_converter(f.m2m_db_table()) in table_names: @@ -258,7 +258,7 @@ def sql_model_create(model, style, known_models=set()): pending_references = {} qn = connection.ops.quote_name inline_references = connection.features.inline_fk_references - for f in opts.fields: + for f in opts.local_fields: col_type = f.db_type() tablespace = f.db_tablespace or opts.db_tablespace if col_type is None: @@ -294,14 +294,8 @@ def sql_model_create(model, style, known_models=set()): style.SQL_COLTYPE(models.IntegerField().db_type()) + ' ' + \ style.SQL_KEYWORD('NULL')) for field_constraints in opts.unique_together: - constraint_output = [style.SQL_KEYWORD('UNIQUE')] - constraint_output.append('(%s)' % \ + table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \ ", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints])) - if opts.db_tablespace and connection.features.supports_tablespaces \ - and connection.features.autoindexes_primary_keys: - constraint_output.append(connection.ops.tablespace_sql( - opts.db_tablespace, inline=True)) - table_output.append(' '.join(constraint_output)) full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] for i, line in enumerate(table_output): # Combine and add commas. @@ -359,7 +353,7 @@ def many_to_many_sql_for_model(model, style): final_output = [] qn = connection.ops.quote_name inline_references = connection.features.inline_fk_references - for f in opts.many_to_many: + for f in opts.local_many_to_many: if not isinstance(f.rel, generic.GenericRel): tablespace = f.db_tablespace or opts.db_tablespace if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: @@ -471,7 +465,7 @@ def sql_indexes_for_model(model, style): output = [] qn = connection.ops.quote_name - for f in model._meta.fields: + for f in model._meta.local_fields: if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys): unique = f.unique and 'UNIQUE ' or '' tablespace = f.db_tablespace or model._meta.db_tablespace diff --git a/django/core/management/validation.py b/django/core/management/validation.py index bc9faae056..cd1f84f34b 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -32,7 +32,7 @@ def get_validation_errors(outfile, app=None): opts = cls._meta # Do field-specific validation. - for f in opts.fields: + for f in opts.local_fields: if f.name == 'id' and not f.primary_key and opts.pk.name == 'id': e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name) if f.name.endswith('_'): @@ -69,8 +69,8 @@ def get_validation_errors(outfile, app=None): if db_version < (5, 0, 3) and isinstance(f, (models.CharField, models.CommaSeparatedIntegerField, models.SlugField)) and f.max_length > 255: e.add(opts, '"%s": %s cannot have a "max_length" greater than 255 when you are using a version of MySQL prior to 5.0.3 (you are using %s).' % (f.name, f.__class__.__name__, '.'.join([str(n) for n in db_version[:3]]))) - # Check to see if the related field will clash with any - # existing fields, m2m fields, m2m related objects or related objects + # Check to see if the related field will clash with any existing + # fields, m2m fields, m2m related objects or related objects if f.rel: if f.rel.to not in models.get_models(): e.add(opts, "'%s' has relation with model %s, which has not been installed" % (f.name, f.rel.to)) @@ -87,7 +87,7 @@ def get_validation_errors(outfile, app=None): 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.many_to_many: + 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: @@ -104,9 +104,10 @@ def get_validation_errors(outfile, app=None): 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)) - for i, f in enumerate(opts.many_to_many): + for i, f in enumerate(opts.local_many_to_many): # Check to see if the related m2m field will clash with any - # existing fields, m2m fields, m2m related objects or related objects + # existing fields, m2m fields, m2m related objects or related + # objects if f.rel.to not in models.get_models(): e.add(opts, "'%s' has m2m relation with model %s, which has not been installed" % (f.name, f.rel.to)) # it is a string and we could not find the model it refers to @@ -117,17 +118,17 @@ 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() - # If rel_name is none, there is no reverse accessor. - # (This only occurs for symmetrical m2m relations to self). - # If this is the case, there are no clashes to check for this field, as - # there are no reverse descriptors for this field. + # If rel_name is none, there is no reverse accessor (this only + # occurs for symmetrical m2m relations to self). If this is the + # case, there are no clashes to check for this field, as there are + # no reverse descriptors for this field. if rel_name is not None: for r in rel_opts.fields: if r.name == rel_name: e.add(opts, "Accessor for m2m 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 m2m 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.many_to_many: + for r in rel_opts.local_many_to_many: if r.name == rel_name: e.add(opts, "Accessor for m2m 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: @@ -200,7 +201,10 @@ def get_validation_errors(outfile, app=None): field_name = field_name[1:] if opts.order_with_respect_to and field_name == '_order': continue - if '.' in field_name: continue # Skip ordering in the format 'table.field'. + # Skip ordering in the format field1__field2 (FIXME: checking + # this format would be nice, but it's a little fiddly). + if '_' in field_name: + continue try: opts.get_field(field_name, many_to_many=False) except models.FieldDoesNotExist: @@ -228,5 +232,7 @@ def get_validation_errors(outfile, app=None): else: if isinstance(f.rel, models.ManyToManyRel): e.add(opts, '"unique_together" refers to %s. ManyToManyFields are not supported in unique_together.' % f.name) + if f not in opts.local_fields: + e.add(opts, '"unique_together" refers to %s. This is not in the same model as the unique_together statement.' % f.name) return len(e.errors) diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 2c92e2afad..a79497ecec 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -165,7 +165,7 @@ class DeserializedObject(object): # This ensures that the data that is deserialized is literally # what came from the file, not post-processed by pre_save/save # methods. - models.Model.save(self.object, raw=True) + models.Model.save_base(self.object, raw=True) if self.m2m_data and save_m2m: for accessor_name, object_list in self.m2m_data.items(): setattr(self.object, accessor_name, object_list) diff --git a/django/db/__init__.py b/django/db/__init__.py index 8f75e0d7b8..95dd36822e 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -11,16 +11,18 @@ if not settings.DATABASE_ENGINE: settings.DATABASE_ENGINE = 'dummy' try: - # Most of the time, the database backend will be one of the official + # Most of the time, the database backend will be one of the official # backends that ships with Django, so look there first. _import_path = 'django.db.backends.' backend = __import__('%s%s.base' % (_import_path, settings.DATABASE_ENGINE), {}, {}, ['']) + creation = __import__('%s%s.creation' % (_import_path, settings.DATABASE_ENGINE), {}, {}, ['']) except ImportError, e: - # If the import failed, we might be looking for a database backend + # If the import failed, we might be looking for a database backend # distributed external to Django. So we'll try that next. try: _import_path = '' backend = __import__('%s.base' % settings.DATABASE_ENGINE, {}, {}, ['']) + creation = __import__('%s.creation' % settings.DATABASE_ENGINE, {}, {}, ['']) except ImportError, e_user: # The database backend wasn't found. Display a helpful error message # listing all possible (built-in) database backends. @@ -37,10 +39,12 @@ def _import_database_module(import_path='', module_name=''): """Lazily import a database module when requested.""" return __import__('%s%s.%s' % (import_path, settings.DATABASE_ENGINE, module_name), {}, {}, ['']) -# We don't want to import the introspect/creation modules unless -# someone asks for 'em, so lazily load them on demmand. +# We don't want to import the introspect module unless someone asks for it, so +# lazily load it on demmand. get_introspection_module = curry(_import_database_module, _import_path, 'introspection') -get_creation_module = curry(_import_database_module, _import_path, 'creation') + +def get_creation_module(): + return creation # We want runshell() to work the same way, but we have to treat it a # little differently (since it just runs instead of returning a module like diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index be1776e65f..8917fc3b23 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -49,7 +49,8 @@ class BaseDatabaseFeatures(object): supports_constraints = True supports_tablespaces = False uses_case_insensitive_names = False - uses_custom_queryset = False + uses_custom_query_class = False + empty_fetchmany_value = [] class BaseDatabaseOperations(object): """ @@ -86,10 +87,9 @@ class BaseDatabaseOperations(object): Returns the SQL necessary to cast a datetime value so that it will be retrieved as a Python datetime object instead of a string. - This SQL should include a '%s' in place of the field's name. This - method should return None if no casting is necessary. + This SQL should include a '%s' in place of the field's name. """ - return None + return "%s" def deferrable_sql(self): """ @@ -169,6 +169,14 @@ class BaseDatabaseOperations(object): sql += " OFFSET %s" % offset return sql + def lookup_cast(self, lookup_type): + """ + Returns the string to use in a query when performing lookups + ("contains", "like", etc). The resulting string should contain a '%s' + placeholder for the column being searched against. + """ + return "%s" + def max_name_length(self): """ Returns the maximum length of table and column names, or None if there @@ -176,6 +184,14 @@ class BaseDatabaseOperations(object): """ return None + def no_limit_value(self): + """ + Returns the value to use for the LIMIT when we are wanting "LIMIT + infinity". Returns None if the limit clause can be omitted in this case. + """ + # FIXME: API may need to change once Oracle backend is repaired. + raise NotImplementedError() + def pk_default_value(self): """ Returns the value to use during an INSERT statement to specify that @@ -183,11 +199,11 @@ class BaseDatabaseOperations(object): """ return 'DEFAULT' - def query_set_class(self, DefaultQuerySet): + def query_class(self, DefaultQueryClass): """ Given the default QuerySet class, returns a custom QuerySet class to use for this backend. Returns None if a custom QuerySet isn't used. - See also BaseDatabaseFeatures.uses_custom_queryset, which regulates + See also BaseDatabaseFeatures.uses_custom_query_class, which regulates whether this method is called at all. """ return None @@ -205,6 +221,17 @@ class BaseDatabaseOperations(object): """ return 'RANDOM()' + def regex_lookup(self, lookup_type): + """ + Returns the string to use in a query when performing regular expression + lookups (using "regex" or "iregex"). The resulting string should + contain a '%s' placeholder for the column being searched against. + + If the feature is not supported (or part of it is not supported), a + NotImplementedError exception can be raised. + """ + raise NotImplementedError + def sql_flush(self, style, tables, sequences): """ Returns a list of SQL statements required to remove all data from diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 7782387f41..17aa6f13bf 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -62,6 +62,7 @@ server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') class DatabaseFeatures(BaseDatabaseFeatures): autoindexes_primary_keys = False inline_fk_references = False + empty_fetchmany_value = () class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): @@ -94,6 +95,10 @@ class DatabaseOperations(BaseDatabaseOperations): sql += "%s," % offset return sql + str(limit) + def no_limit_value(self): + # 2**64 - 1, as recommended by the MySQL documentation + return 18446744073709551615L + def quote_name(self, name): if name.startswith("`") and name.endswith("`"): return name # Quoting once is enough. diff --git a/django/db/backends/mysql_old/base.py b/django/db/backends/mysql_old/base.py index c22094b968..efbfeeafc5 100644 --- a/django/db/backends/mysql_old/base.py +++ b/django/db/backends/mysql_old/base.py @@ -66,6 +66,7 @@ class MysqlDebugWrapper: class DatabaseFeatures(BaseDatabaseFeatures): autoindexes_primary_keys = False inline_fk_references = False + empty_fetchmany_value = () class DatabaseOperations(BaseDatabaseOperations): def date_extract_sql(self, lookup_type, field_name): @@ -98,6 +99,10 @@ class DatabaseOperations(BaseDatabaseOperations): sql += "%s," % offset return sql + str(limit) + def no_limit_value(self): + # 2**64 - 1, as recommended by the MySQL documentation + return 18446744073709551615L + def quote_name(self, name): if name.startswith("`") and name.endswith("`"): return name # Quoting once is enough. diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 152adf7056..3635acdf2a 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -4,11 +4,12 @@ Oracle database backend for Django. Requires cx_Oracle: http://www.python.net/crew/atuining/cx_Oracle/ """ +import os + from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util +from django.db.backends.oracle import query from django.utils.datastructures import SortedDict from django.utils.encoding import smart_str, force_unicode -import datetime -import os # Oracle takes client-side character set encoding from the environment. os.environ['NLS_LANG'] = '.UTF8' @@ -24,11 +25,12 @@ IntegrityError = Database.IntegrityError class DatabaseFeatures(BaseDatabaseFeatures): allows_group_by_ordinal = False allows_unique_and_pk = False # Suppress UNIQUE/PK for Oracle (ORA-02259) + empty_fetchmany_value = () needs_datetime_string_cast = False needs_upper_for_iops = True supports_tablespaces = True uses_case_insensitive_names = True - uses_custom_queryset = True + uses_custom_query_class = True class DatabaseOperations(BaseDatabaseOperations): def autoinc_sql(self, table, column): @@ -89,243 +91,16 @@ class DatabaseOperations(BaseDatabaseOperations): # Instead, they are handled in django/db/backends/oracle/query.py. return "" + def lookup_cast(self, lookup_type): + if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): + return "UPPER(%s)" + return "%s" + def max_name_length(self): return 30 - def query_set_class(self, DefaultQuerySet): - from django.db import connection - from django.db.models.query import EmptyResultSet, GET_ITERATOR_CHUNK_SIZE, quote_only_if_word - - class OracleQuerySet(DefaultQuerySet): - - def iterator(self): - "Performs the SELECT database lookup of this QuerySet." - - from django.db.models.query import get_cached_row - - # self._select is a dictionary, and dictionaries' key order is - # undefined, so we convert it to a list of tuples. - extra_select = self._select.items() - - full_query = None - - try: - try: - select, sql, params, full_query = self._get_sql_clause(get_full_query=True) - except TypeError: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - raise StopIteration - if not full_query: - full_query = "SELECT %s%s\n%s" % ((self._distinct and "DISTINCT " or ""), ', '.join(select), sql) - - cursor = connection.cursor() - cursor.execute(full_query, params) - - fill_cache = self._select_related - fields = self.model._meta.fields - index_end = len(fields) - - # so here's the logic; - # 1. retrieve each row in turn - # 2. convert NCLOBs - - while 1: - rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) - if not rows: - raise StopIteration - for row in rows: - row = self.resolve_columns(row, fields) - if fill_cache: - obj, index_end = get_cached_row(klass=self.model, row=row, - index_start=0, max_depth=self._max_related_depth) - else: - obj = self.model(*row[:index_end]) - for i, k in enumerate(extra_select): - setattr(obj, k[0], row[index_end+i]) - yield obj - - - def _get_sql_clause(self, get_full_query=False): - from django.db.models.query import fill_table_cache, \ - handle_legacy_orderlist, orderfield2column - - opts = self.model._meta - qn = connection.ops.quote_name - - # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. - select = ["%s.%s" % (qn(opts.db_table), qn(f.column)) for f in opts.fields] - tables = [quote_only_if_word(t) for t in self._tables] - joins = SortedDict() - where = self._where[:] - params = self._params[:] - - # Convert self._filters into SQL. - joins2, where2, params2 = self._filters.get_sql(opts) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - - # Add additional tables and WHERE clauses based on select_related. - if self._select_related: - fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table]) - - # Add any additional SELECTs. - if self._select: - select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), qn(s[0])) for s in self._select.items()]) - - # Start composing the body of the SQL statement. - sql = [" FROM", qn(opts.db_table)] - - # Compose the join dictionary into SQL describing the joins. - if joins: - sql.append(" ".join(["%s %s %s ON %s" % (join_type, table, alias, condition) - for (alias, (table, join_type, condition)) in joins.items()])) - - # Compose the tables clause into SQL. - if tables: - sql.append(", " + ", ".join(tables)) - - # Compose the where clause into SQL. - if where: - sql.append(where and "WHERE " + " AND ".join(where)) - - # ORDER BY clause - order_by = [] - if self._order_by is not None: - ordering_to_use = self._order_by - else: - ordering_to_use = opts.ordering - for f in handle_legacy_orderlist(ordering_to_use): - if f == '?': # Special case. - order_by.append(DatabaseOperations().random_function_sql()) - else: - if f.startswith('-'): - col_name = f[1:] - order = "DESC" - else: - col_name = f - order = "ASC" - if "." in col_name: - table_prefix, col_name = col_name.split('.', 1) - table_prefix = qn(table_prefix) + '.' - else: - # Use the database table as a column prefix if it wasn't given, - # and if the requested column isn't a custom SELECT. - if "." not in col_name and col_name not in (self._select or ()): - table_prefix = qn(opts.db_table) + '.' - else: - table_prefix = '' - order_by.append('%s%s %s' % (table_prefix, qn(orderfield2column(col_name, opts)), order)) - if order_by: - sql.append("ORDER BY " + ", ".join(order_by)) - - # Look for column name collisions in the select elements - # and fix them with an AS alias. This allows us to do a - # SELECT * later in the paging query. - cols = [clause.split('.')[-1] for clause in select] - for index, col in enumerate(cols): - if cols.count(col) > 1: - col = '%s%d' % (col.replace('"', ''), index) - cols[index] = col - select[index] = '%s AS %s' % (select[index], col) - - # LIMIT and OFFSET clauses - # To support limits and offsets, Oracle requires some funky rewriting of an otherwise normal looking query. - select_clause = ",".join(select) - distinct = (self._distinct and "DISTINCT " or "") - - if order_by: - order_by_clause = " OVER (ORDER BY %s )" % (", ".join(order_by)) - else: - #Oracle's row_number() function always requires an order-by clause. - #So we need to define a default order-by, since none was provided. - order_by_clause = " OVER (ORDER BY %s.%s)" % \ - (qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column)) - # limit_and_offset_clause - if self._limit is None: - assert self._offset is None, "'offset' is not allowed without 'limit'" - - if self._offset is not None: - offset = int(self._offset) - else: - offset = 0 - if self._limit is not None: - limit = int(self._limit) - else: - limit = None - - limit_and_offset_clause = '' - if limit is not None: - limit_and_offset_clause = "WHERE rn > %s AND rn <= %s" % (offset, limit+offset) - elif offset: - limit_and_offset_clause = "WHERE rn > %s" % (offset) - - if len(limit_and_offset_clause) > 0: - fmt = \ - """SELECT * FROM - (SELECT %s%s, - ROW_NUMBER()%s AS rn - %s) - %s""" - full_query = fmt % (distinct, select_clause, - order_by_clause, ' '.join(sql).strip(), - limit_and_offset_clause) - else: - full_query = None - - if get_full_query: - return select, " ".join(sql), params, full_query - else: - return select, " ".join(sql), params - - def resolve_columns(self, row, fields=()): - from django.db.models.fields import DateField, DateTimeField, \ - TimeField, BooleanField, NullBooleanField, DecimalField, Field - values = [] - for value, field in map(None, row, fields): - if isinstance(value, Database.LOB): - value = value.read() - # Oracle stores empty strings as null. We need to undo this in - # order to adhere to the Django convention of using the empty - # string instead of null, but only if the field accepts the - # empty string. - if value is None and isinstance(field, Field) and field.empty_strings_allowed: - value = u'' - # Convert 1 or 0 to True or False - elif value in (1, 0) and isinstance(field, (BooleanField, NullBooleanField)): - value = bool(value) - # Convert floats to decimals - elif value is not None and isinstance(field, DecimalField): - value = util.typecast_decimal(field.format_number(value)) - # cx_Oracle always returns datetime.datetime objects for - # DATE and TIMESTAMP columns, but Django wants to see a - # python datetime.date, .time, or .datetime. We use the type - # of the Field to determine which to cast to, but it's not - # always available. - # As a workaround, we cast to date if all the time-related - # values are 0, or to time if the date is 1/1/1900. - # This could be cleaned a bit by adding a method to the Field - # classes to normalize values from the database (the to_python - # method is used for validation and isn't what we want here). - elif isinstance(value, Database.Timestamp): - # In Python 2.3, the cx_Oracle driver returns its own - # Timestamp object that we must convert to a datetime class. - if not isinstance(value, datetime.datetime): - value = datetime.datetime(value.year, value.month, value.day, value.hour, - value.minute, value.second, value.fsecond) - if isinstance(field, DateTimeField): - pass # DateTimeField subclasses DateField so must be checked first. - elif isinstance(field, DateField): - value = value.date() - elif isinstance(field, TimeField) or (value.year == 1900 and value.month == value.day == 1): - value = value.time() - elif value.hour == value.minute == value.second == value.microsecond == 0: - value = value.date() - values.append(value) - return values - - return OracleQuerySet + def query_class(self, DefaultQueryClass): + return query.query_class(DefaultQueryClass, Database) def quote_name(self, name): # SQL92 requires delimited (quoted) names to be case-sensitive. When @@ -339,6 +114,23 @@ class DatabaseOperations(BaseDatabaseOperations): def random_function_sql(self): return "DBMS_RANDOM.RANDOM" + def regex_lookup_9(self, lookup_type): + raise NotImplementedError("Regexes are not supported in Oracle before version 10g.") + + def regex_lookup_10(self, lookup_type): + if lookup_type == 'regex': + match_option = "'c'" + else: + match_option = "'i'" + return 'REGEXP_LIKE(%%s, %%s, %s)' % match_option + + def regex_lookup(self, lookup_type): + # If regex_lookup is called before it's been initialized, then create + # a cursor to initialize it and recur. + from django.db import connection + connection.cursor() + return connection.ops.regex_lookup(lookup_type) + def sql_flush(self, style, tables, sequences): # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', # 'TRUNCATE z;'... style SQL statements @@ -430,6 +222,14 @@ class DatabaseWrapper(BaseDatabaseWrapper): "NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'") try: self.oracle_version = int(self.connection.version.split('.')[0]) + # There's no way for the DatabaseOperations class to know the + # currently active Oracle version, so we do some setups here. + # TODO: Multi-db support will need a better solution (a way to + # communicate the current version). + if self.oracle_version <= 9: + self.ops.regex_lookup = self.ops.regex_lookup_9 + else: + self.ops.regex_lookup = self.ops.regex_lookup_10 except ValueError: pass try: diff --git a/django/db/backends/oracle/query.py b/django/db/backends/oracle/query.py new file mode 100644 index 0000000000..033ffe8533 --- /dev/null +++ b/django/db/backends/oracle/query.py @@ -0,0 +1,151 @@ +""" +Custom Query class for this backend (a derivative of +django.db.models.sql.query.Query). +""" + +import datetime + +from django.db.backends import util + +# Cache. Maps default query class to new Oracle query class. +_classes = {} + +def query_class(QueryClass, Database): + """ + Returns a custom djang.db.models.sql.query.Query subclass that is + appropraite for Oracle. + + The 'Database' module (cx_Oracle) is passed in here so that all the setup + required to import it only needs to be done by the calling module. + """ + global _classes + try: + return _classes[QueryClass] + except KeyError: + pass + + class OracleQuery(QueryClass): + def resolve_columns(self, row, fields=()): + index_start = len(self.extra_select.keys()) + values = [self.convert_values(v, None) for v in row[:index_start]] + for value, field in map(None, row[index_start:], fields): + values.append(self.convert_values(value, field)) + return values + + def convert_values(self, value, field): + from django.db.models.fields import DateField, DateTimeField, \ + TimeField, BooleanField, NullBooleanField, DecimalField, Field + if isinstance(value, Database.LOB): + value = value.read() + # Oracle stores empty strings as null. We need to undo this in + # order to adhere to the Django convention of using the empty + # string instead of null, but only if the field accepts the + # empty string. + if value is None and isinstance(field, Field) and field.empty_strings_allowed: + value = u'' + # Convert 1 or 0 to True or False + elif value in (1, 0) and isinstance(field, (BooleanField, NullBooleanField)): + value = bool(value) + # Convert floats to decimals + elif value is not None and isinstance(field, DecimalField): + value = util.typecast_decimal(field.format_number(value)) + # cx_Oracle always returns datetime.datetime objects for + # DATE and TIMESTAMP columns, but Django wants to see a + # python datetime.date, .time, or .datetime. We use the type + # of the Field to determine which to cast to, but it's not + # always available. + # As a workaround, we cast to date if all the time-related + # values are 0, or to time if the date is 1/1/1900. + # This could be cleaned a bit by adding a method to the Field + # classes to normalize values from the database (the to_python + # method is used for validation and isn't what we want here). + elif isinstance(value, Database.Timestamp): + # In Python 2.3, the cx_Oracle driver returns its own + # Timestamp object that we must convert to a datetime class. + if not isinstance(value, datetime.datetime): + value = datetime.datetime(value.year, value.month, + value.day, value.hour, value.minute, value.second, + value.fsecond) + if isinstance(field, DateTimeField): + # DateTimeField subclasses DateField so must be checked + # first. + pass + elif isinstance(field, DateField): + value = value.date() + elif isinstance(field, TimeField) or (value.year == 1900 and value.month == value.day == 1): + value = value.time() + elif value.hour == value.minute == value.second == value.microsecond == 0: + value = value.date() + return value + + def as_sql(self, with_limits=True, with_col_aliases=False): + """ + Creates the SQL for this query. Returns the SQL string and list + of parameters. This is overriden from the original Query class + to accommodate Oracle's limit/offset SQL. + + If 'with_limits' is False, any limit/offset information is not + included in the query. + """ + # The `do_offset` flag indicates whether we need to construct + # the SQL needed to use limit/offset w/Oracle. + do_offset = with_limits and (self.high_mark or self.low_mark) + + # If no offsets, just return the result of the base class + # `as_sql`. + if not do_offset: + return super(OracleQuery, self).as_sql(with_limits=False, + with_col_aliases=with_col_aliases) + + # `get_columns` needs to be called before `get_ordering` to + # populate `_select_alias`. + self.pre_sql_setup() + out_cols = self.get_columns() + ordering = self.get_ordering() + + # Getting the "ORDER BY" SQL for the ROW_NUMBER() result. + if ordering: + rn_orderby = ', '.join(ordering) + else: + # Oracle's ROW_NUMBER() function always requires an + # order-by clause. So we need to define a default + # order-by, since none was provided. + qn = self.quote_name_unless_alias + opts = self.model._meta + rn_orderby = '%s.%s' % (qn(opts.db_table), qn(opts.fields[0].db_column or opts.fields[0].column)) + + # Getting the selection SQL and the params, which has the `rn` + # extra selection SQL. + self.extra_select['rn'] = 'ROW_NUMBER() OVER (ORDER BY %s )' % rn_orderby + sql, params= super(OracleQuery, self).as_sql(with_limits=False, + with_col_aliases=True) + + # Constructing the result SQL, using the initial select SQL + # obtained above. + result = ['SELECT * FROM (%s)' % sql] + + # Place WHERE condition on `rn` for the desired range. + result.append('WHERE rn > %d' % self.low_mark) + if self.high_mark: + result.append('AND rn <= %d' % self.high_mark) + + # Returning the SQL w/params. + return ' '.join(result), params + + def set_limits(self, low=None, high=None): + super(OracleQuery, self).set_limits(low, high) + + # We need to select the row number for the LIMIT/OFFSET sql. + # A placeholder is added to extra_select now, because as_sql is + # too late to be modifying extra_select. However, the actual sql + # depends on the ordering, so that is generated in as_sql. + self.extra_select['rn'] = '1' + + def clear_limits(self): + super(OracleQuery, self).clear_limits() + if 'rn' in self.extra_select: + del self.extra_select['rn'] + + _classes[QueryClass] = OracleQuery + return OracleQuery + diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index cd46413453..7e27b23f72 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -44,6 +44,9 @@ class DatabaseOperations(BaseDatabaseOperations): cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name)) return cursor.fetchone()[0] + def no_limit_value(self): + return None + def quote_name(self, name): if name.startswith('"') and name.endswith('"'): return name # Quoting once is enough. diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index b4b445cd16..b8bf5c8f0b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -63,6 +63,9 @@ class DatabaseOperations(BaseDatabaseOperations): return name # Quoting once is enough. return '"%s"' % name + def no_limit_value(self): + return -1 + def sql_flush(self, style, tables, sequences): # NB: The generated SQL below is specific to SQLite # Note: The DELETE FROM... SQL generated below works for SQLite databases diff --git a/django/db/models/base.py b/django/db/models/base.py index d96c93fb0d..bb02d7a00c 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1,10 +1,16 @@ -import django.db.models.manipulators -import django.db.models.manager +import copy +import types +import sys +import os +from itertools import izip + +import django.db.models.manipulators # Imported to register signal handler. +import django.db.models.manager # Ditto. from django.core import validators -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist -from django.db.models.fields.related import OneToOneRel, ManyToOneRel -from django.db.models.query import delete_objects +from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField +from django.db.models.query import delete_objects, Q from django.db.models.options import Options, AdminOptions from django.db import connection, transaction from django.db.models import signals @@ -14,10 +20,11 @@ from django.utils.datastructures import SortedDict from django.utils.functional import curry from django.utils.encoding import smart_str, force_unicode, smart_unicode from django.conf import settings -from itertools import izip -import types -import sys -import os + +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback class ModelBase(type): "Metaclass for all models" @@ -25,29 +32,45 @@ class ModelBase(type): # If this isn't a subclass of Model, don't do anything special. try: parents = [b for b in bases if issubclass(b, Model)] - if not parents: - return super(ModelBase, cls).__new__(cls, name, bases, attrs) except NameError: # 'Model' isn't defined yet, meaning we're looking at Django's own # Model class, defined below. + parents = [] + if not parents: return super(ModelBase, cls).__new__(cls, name, bases, attrs) # Create the class. - new_class = type.__new__(cls, name, bases, {'__module__': attrs.pop('__module__')}) - new_class.add_to_class('_meta', Options(attrs.pop('Meta', None))) - new_class.add_to_class('DoesNotExist', types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {})) - new_class.add_to_class('MultipleObjectsReturned', - types.ClassType('MultipleObjectsReturned', (MultipleObjectsReturned, ), {})) - - # Build complete list of parents - for base in parents: - # Things without _meta aren't functional models, so they're - # uninteresting parents. - if hasattr(base, '_meta'): - new_class._meta.parents.append(base) - new_class._meta.parents.extend(base._meta.parents) + module = attrs.pop('__module__') + new_class = type.__new__(cls, name, bases, {'__module__': module}) + attr_meta = attrs.pop('Meta', None) + abstract = getattr(attr_meta, 'abstract', False) + if not attr_meta: + meta = getattr(new_class, 'Meta', None) + else: + meta = attr_meta + base_meta = getattr(new_class, '_meta', None) + new_class.add_to_class('_meta', Options(meta)) + if not abstract: + new_class.add_to_class('DoesNotExist', + subclass_exception('DoesNotExist', ObjectDoesNotExist, module)) + new_class.add_to_class('MultipleObjectsReturned', + subclass_exception('MultipleObjectsReturned', MultipleObjectsReturned, module)) + if base_meta and not base_meta.abstract: + # Non-abstract child classes inherit some attributes from their + # non-abstract parent (unless an ABC comes before it in the + # method resolution order). + if not hasattr(meta, 'ordering'): + new_class._meta.ordering = base_meta.ordering + if not hasattr(meta, 'get_latest_by'): + new_class._meta.get_latest_by = base_meta.get_latest_by + old_default_mgr = None + if getattr(new_class, '_default_manager', None): + # We have a parent who set the default manager. + if new_class._default_manager.model._meta.abstract: + old_default_mgr = new_class._default_manager + new_class._default_manager = None if getattr(new_class._meta, 'app_label', None) is None: # Figure out the app_label by looking one level up. # For 'django.contrib.sites.models', this would be 'sites'. @@ -63,21 +86,50 @@ class ModelBase(type): for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) - # Add Fields inherited from parents - for parent in new_class._meta.parents: - for field in parent._meta.fields: - # Only add parent fields if they aren't defined for this class. - try: - new_class._meta.get_field(field.name) - except FieldDoesNotExist: - field.contribute_to_class(new_class, field.name) + # Do the appropriate setup for any model parents. + o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields + if isinstance(f, OneToOneField)]) + for base in parents: + if not hasattr(base, '_meta'): + # Things without _meta aren't functional models, so they're + # uninteresting parents. + continue + if not base._meta.abstract: + if base in o2o_map: + field = o2o_map[base] + field.primary_key = True + new_class._meta.setup_pk(field) + else: + attr_name = '%s_ptr' % base._meta.module_name + field = OneToOneField(base, name=attr_name, + auto_created=True, parent_link=True) + new_class.add_to_class(attr_name, field) + new_class._meta.parents[base] = field + else: + # The abstract base class case. + names = set([f.name for f in new_class._meta.local_fields + new_class._meta.many_to_many]) + for field in base._meta.local_fields + base._meta.local_many_to_many: + if field.name in names: + raise FieldError('Local field %r in class %r clashes with field of similar name from abstract base class %r' + % (field.name, name, base.__name__)) + new_class.add_to_class(field.name, copy.deepcopy(field)) + if abstract: + # Abstract base models can't be instantiated and don't appear in + # the list of models for an app. We do the final setup for them a + # little differently from normal models. + attr_meta.abstract = False + new_class.Meta = attr_meta + return new_class + + if old_default_mgr and not new_class._default_manager: + new_class._default_manager = old_default_mgr._copy_to_model(new_class) new_class._prepare() - register_models(new_class._meta.app_label, new_class) + # Because of the way imports happen (recursively), we may or may not be - # the first class for this model to register with the framework. There - # should only be one class for each model, so we must always return the + # the first time this model tries to register with the framework. There + # should only be one class for each model, so we always return the # registered version. return get_model(new_class._meta.app_label, name, False) @@ -113,31 +165,6 @@ class ModelBase(type): class Model(object): __metaclass__ = ModelBase - def _get_pk_val(self): - return getattr(self, self._meta.pk.attname) - - def _set_pk_val(self, value): - return setattr(self, self._meta.pk.attname, value) - - pk = property(_get_pk_val, _set_pk_val) - - def __repr__(self): - return smart_str(u'<%s: %s>' % (self.__class__.__name__, unicode(self))) - - def __str__(self): - if hasattr(self, '__unicode__'): - return force_unicode(self).encode('utf-8') - return '%s object' % self.__class__.__name__ - - def __eq__(self, other): - return isinstance(other, self.__class__) and self._get_pk_val() == other._get_pk_val() - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self._get_pk_val()) - def __init__(self, *args, **kwargs): dispatcher.send(signal=signals.pre_init, sender=self.__class__, args=args, kwargs=kwargs) @@ -210,74 +237,133 @@ class Model(object): raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] dispatcher.send(signal=signals.post_init, sender=self.__class__, instance=self) - def save(self, raw=False): - dispatcher.send(signal=signals.pre_save, sender=self.__class__, - instance=self, raw=raw) + def from_sequence(cls, values): + """ + An alternate class constructor, primarily for internal use. - non_pks = [f for f in self._meta.fields if not f.primary_key] - cursor = connection.cursor() + Creates a model instance from a sequence of values (which corresponds + to all the non-many-to-many fields in creation order. If there are more + fields than values, the remaining (final) fields are given their + default values. - qn = connection.ops.quote_name + ForeignKey fields can only be initialised using id values, not + instances, in this method. + """ + dispatcher.send(signal=signals.pre_init, sender=cls, args=values, + kwargs={}) + obj = Empty() + obj.__class__ = cls + field_iter = iter(obj._meta.fields) + for val, field in izip(values, field_iter): + setattr(obj, field.attname, val) + for field in field_iter: + setattr(obj, field.attname, field.get_default()) + dispatcher.send(signal=signals.post_init, sender=cls, instance=obj) + return obj + + from_sequence = classmethod(from_sequence) + + def __repr__(self): + return smart_str(u'<%s: %s>' % (self.__class__.__name__, unicode(self))) + + def __str__(self): + if hasattr(self, '__unicode__'): + return force_unicode(self).encode('utf-8') + return '%s object' % self.__class__.__name__ + + def __eq__(self, other): + return isinstance(other, self.__class__) and self._get_pk_val() == other._get_pk_val() + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self._get_pk_val()) + + def _get_pk_val(self, meta=None): + if not meta: + meta = self._meta + return getattr(self, meta.pk.attname) + + def _set_pk_val(self, value): + return setattr(self, self._meta.pk.attname, value) + + pk = property(_get_pk_val, _set_pk_val) + + def save(self): + """ + Save the current instance. Override this in a subclass if you want to + control the saving process. + """ + self.save_base() + + save.alters_data = True + + def save_base(self, raw=False, cls=None): + """ + Does the heavy-lifting involved in saving. Subclasses shouldn't need to + override this method. It's separate from save() in order to hide the + need for overrides of save() to pass around internal-only parameters + ('raw' and 'cls'). + """ + if not cls: + cls = self.__class__ + meta = self._meta + signal = True + dispatcher.send(signal=signals.pre_save, sender=self.__class__, + instance=self, raw=raw) + else: + meta = cls._meta + signal = False + + for parent, field in meta.parents.items(): + self.save_base(raw, parent) + setattr(self, field.attname, self._get_pk_val(parent._meta)) + + non_pks = [f for f in meta.local_fields if not f.primary_key] # First, try an UPDATE. If that doesn't update anything, do an INSERT. - pk_val = self._get_pk_val() + pk_val = self._get_pk_val(meta) # Note: the comparison with '' is required for compatibility with # oldforms-style model creation. pk_set = pk_val is not None and smart_unicode(pk_val) != u'' record_exists = True + manager = cls._default_manager if pk_set: # Determine whether a record with the primary key already exists. - cursor.execute("SELECT 1 FROM %s WHERE %s=%%s" % \ - (qn(self._meta.db_table), qn(self._meta.pk.column)), - self._meta.pk.get_db_prep_lookup('exact', pk_val)) - # If it does already exist, do an UPDATE. - if cursor.fetchone(): - db_values = [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, False)) for f in non_pks] - placeholders = [f.get_placeholder(raw and getattr(self, f.attname) or f.pre_save(self, False)) for f in non_pks] - if db_values: - cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \ - (qn(self._meta.db_table), - ','.join(['%s=%s' % (qn(f.column), placeholders[i]) for i, f in enumerate(non_pks)]), - qn(self._meta.pk.column)), - db_values + self._meta.pk.get_db_prep_lookup('exact', pk_val)) + if manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by(): + # It does already exist, so do an UPDATE. + if non_pks: + values = [(f, None, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] + manager.filter(pk=pk_val)._update(values) else: record_exists = False if not pk_set or not record_exists: - field_names = [qn(f.column) for f in self._meta.fields if not isinstance(f, AutoField)] - db_values = [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True)) for f in self._meta.fields if not isinstance(f, AutoField)] - placeholders = [f.get_placeholder(f.pre_save(self, True)) for f in self._meta.fields if not isinstance(f, AutoField)] - # If the PK has been manually set, respect that. - if pk_set: - field_names += [f.column for f in self._meta.fields if isinstance(f, AutoField)] - db_values += [f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True)) for f in self._meta.fields if isinstance(f, AutoField)] - placeholders += [f.get_placeholder(f.pre_save(self, True)) for f in self._meta.fields if isinstance(f, AutoField)] - if self._meta.order_with_respect_to: - field_names.append(qn('_order')) - placeholders.append('%s') - subsel = 'SELECT COUNT(*) FROM %s WHERE %s = %%s' % ( - qn(self._meta.db_table), - qn(self._meta.order_with_respect_to.column)) - cursor.execute(subsel, (getattr(self, self._meta.order_with_respect_to.attname),)) - db_values.append(cursor.fetchone()[0]) + if not pk_set: + values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)] + else: + values = [(f, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields] + + if meta.order_with_respect_to: + field = meta.order_with_respect_to + values.append((meta.get_field_by_name('_order')[0], manager.filter(**{field.name: getattr(self, field.attname)}).count())) record_exists = False - if db_values: - cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \ - (qn(self._meta.db_table), ','.join(field_names), - ','.join(placeholders)), db_values) + + update_pk = bool(meta.has_auto_field and not pk_set) + if values: + # Create a new record. + result = manager._insert(values, return_id=update_pk) else: # Create a new record with defaults for everything. - cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % - (qn(self._meta.db_table), qn(self._meta.pk.column), - connection.ops.pk_default_value())) - if self._meta.has_auto_field and not pk_set: - setattr(self, self._meta.pk.attname, connection.ops.last_insert_id(cursor, self._meta.db_table, self._meta.pk.column)) + result = manager._insert([(meta.pk, connection.ops.pk_default_value())], return_id=update_pk, raw_values=True) + + if update_pk: + setattr(self, meta.pk.attname, result) transaction.commit_unless_managed() - # Run any post-save hooks. - dispatcher.send(signal=signals.post_save, sender=self.__class__, - instance=self, created=(not record_exists), raw=raw) - - save.alters_data = True + if signal: + dispatcher.send(signal=signals.post_save, sender=self.__class__, + instance=self, created=(not record_exists), raw=raw) def validate(self): """ @@ -343,32 +429,31 @@ class Model(object): return force_unicode(dict(field.choices).get(value, value), strings_only=True) def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs): - qn = connection.ops.quote_name - op = is_next and '>' or '<' - where = '(%s %s %%s OR (%s = %%s AND %s.%s %s %%s))' % \ - (qn(field.column), op, qn(field.column), - qn(self._meta.db_table), qn(self._meta.pk.column), op) + op = is_next and 'gt' or 'lt' + order = not is_next and '-' or '' param = smart_str(getattr(self, field.attname)) - q = self.__class__._default_manager.filter(**kwargs).order_by((not is_next and '-' or '') + field.name, (not is_next and '-' or '') + self._meta.pk.name) - q._where.append(where) - q._params.extend([param, param, getattr(self, self._meta.pk.attname)]) + q = Q(**{'%s__%s' % (field.name, op): param}) + q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk}) + qs = self.__class__._default_manager.filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order) try: - return q[0] + return qs[0] except IndexError: raise self.DoesNotExist, "%s matching query does not exist." % self.__class__._meta.object_name def _get_next_or_previous_in_order(self, is_next): - qn = connection.ops.quote_name cachename = "__%s_order_cache" % is_next if not hasattr(self, cachename): + qn = connection.ops.quote_name op = is_next and '>' or '<' + order = not is_next and '-_order' or '_order' order_field = self._meta.order_with_respect_to + # FIXME: When querysets support nested queries, this can be turned + # into a pure queryset operation. where = ['%s %s (SELECT %s FROM %s WHERE %s=%%s)' % \ (qn('_order'), op, qn('_order'), - qn(self._meta.db_table), qn(self._meta.pk.column)), - '%s=%%s' % qn(order_field.column)] - params = [self._get_pk_val(), getattr(self, order_field.attname)] - obj = self._default_manager.order_by('_order').extra(where=where, params=params)[:1].get() + qn(self._meta.db_table), qn(self._meta.pk.column))] + params = [self.pk] + obj = self._default_manager.filter(**{order_field.name: getattr(self, order_field.attname)}).extra(where=where, params=params).order_by(order)[:1].get() setattr(self, cachename, obj) return getattr(self, cachename) @@ -448,29 +533,20 @@ class Model(object): # ORDERING METHODS ######################### def method_set_order(ordered_obj, self, id_list): - qn = connection.ops.quote_name - cursor = connection.cursor() - # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" - sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \ - (qn(ordered_obj._meta.db_table), qn('_order'), - qn(ordered_obj._meta.order_with_respect_to.column), - qn(ordered_obj._meta.pk.column)) rel_val = getattr(self, ordered_obj._meta.order_with_respect_to.rel.field_name) - cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) + order_name = ordered_obj._meta.order_with_respect_to.name + # FIXME: It would be nice if there was an "update many" version of update + # for situations like this. + for i, j in enumerate(id_list): + ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i) transaction.commit_unless_managed() def method_get_order(ordered_obj, self): - qn = connection.ops.quote_name - cursor = connection.cursor() - # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" - sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY %s" % \ - (qn(ordered_obj._meta.pk.column), - qn(ordered_obj._meta.db_table), - qn(ordered_obj._meta.order_with_respect_to.column), - qn('_order')) rel_val = getattr(self, ordered_obj._meta.order_with_respect_to.rel.field_name) - cursor.execute(sql, [rel_val]) - return [r[0] for r in cursor.fetchall()] + order_name = ordered_obj._meta.order_with_respect_to.name + pk_name = ordered_obj._meta.pk.name + return [r[pk_name] for r in + ordered_obj.objects.filter(**{order_name: rel_val}).values(pk_name)] ############################################## # HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) # @@ -478,3 +554,20 @@ def method_get_order(ordered_obj, self): def get_absolute_url(opts, func, self, *args, **kwargs): return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self, *args, **kwargs) + +######## +# MISC # +######## + +class Empty(object): + pass + +if sys.version_info < (2, 5): + # Prior to Python 2.5, Exception was an old-style class + def subclass_exception(name, parent, unused): + return types.ClassType(name, (parent,), {}) + +else: + def subclass_exception(name, parent, module): + return type(name, (parent,), {'__module__': module}) + diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 45d437a63a..3c9169d53b 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1,3 +1,4 @@ +import copy import datetime import os import time @@ -75,15 +76,19 @@ class Field(object): # database level. empty_strings_allowed = True - # Tracks each time a Field instance is created. Used to retain order. + # These track each time a Field instance is created. Used to retain order. + # The auto_creation_counter is used for fields that Django implicitly + # creates, creation_counter is used for all user-specified fields. creation_counter = 0 + auto_creation_counter = -1 def __init__(self, verbose_name=None, name=None, primary_key=False, - max_length=None, unique=False, blank=False, null=False, db_index=False, - core=False, rel=None, default=NOT_PROVIDED, editable=True, serialize=True, - prepopulate_from=None, unique_for_date=None, unique_for_month=None, - unique_for_year=None, validator_list=None, choices=None, radio_admin=None, - help_text='', db_column=None, db_tablespace=None): + max_length=None, unique=False, blank=False, null=False, + db_index=False, core=False, rel=None, default=NOT_PROVIDED, + editable=True, serialize=True, prepopulate_from=None, + unique_for_date=None, unique_for_month=None, unique_for_year=None, + validator_list=None, choices=None, radio_admin=None, help_text='', + db_column=None, db_tablespace=None, auto_created=False): self.name = name self.verbose_name = verbose_name self.primary_key = primary_key @@ -109,14 +114,27 @@ class Field(object): # Set db_index to True if the field has a relationship and doesn't explicitly set db_index. self.db_index = db_index - # Increase the creation counter, and save our local copy. - self.creation_counter = Field.creation_counter - Field.creation_counter += 1 + # Adjust the appropriate creation counter, and save our local copy. + if auto_created: + self.creation_counter = Field.auto_creation_counter + Field.auto_creation_counter -= 1 + else: + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 def __cmp__(self, other): # This is needed because bisect does not take a comparison function. return cmp(self.creation_counter, other.creation_counter) + def __deepcopy__(self, memodict): + # We don't have to deepcopy very much here, since most things are not + # intended to be altered after initial creation. + obj = copy.copy(self) + if self.rel: + obj.rel = copy.copy(self.rel) + memodict[id(self)] = obj + return obj + def to_python(self, value): """ Converts the input value into the expected Python data type, raising @@ -145,11 +163,10 @@ class Field(object): # mapped to one of the built-in Django field types. In this case, you # can implement db_type() instead of get_internal_type() to specify # exactly which wacky database column type you want to use. - data_types = get_creation_module().DATA_TYPES - internal_type = self.get_internal_type() - if internal_type not in data_types: + try: + return get_creation_module().DATA_TYPES[self.get_internal_type()] % self.__dict__ + except KeyError: return None - return data_types[internal_type] % self.__dict__ def validate_full(self, field_data, all_data): """ diff --git a/django/db/models/fields/proxy.py b/django/db/models/fields/proxy.py new file mode 100644 index 0000000000..31a31e3c3c --- /dev/null +++ b/django/db/models/fields/proxy.py @@ -0,0 +1,16 @@ +""" +Field-like classes that aren't really fields. It's easier to use objects that +have the same attributes as fields sometimes (avoids a lot of special casing). +""" + +from django.db.models import fields + +class OrderWrt(fields.IntegerField): + """ + A proxy for the _order database field that is used when + Meta.order_with_respect_to is specified. + """ + name = '_order' + attname = '_order' + column = '_order' + diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 184d0127f1..615335c30f 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -2,6 +2,7 @@ from django.db import connection, transaction from django.db.models import signals, get_model from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class from django.db.models.related import RelatedObject +from django.db.models.query_utils import QueryWrapper from django.utils.text import capfirst from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _ from django.utils.functional import curry @@ -27,21 +28,21 @@ def add_lazy_relation(cls, field, relation): """ Adds a lookup on ``cls`` when a related field is defined using a string, i.e.:: - + class MyModel(Model): fk = ForeignKey("AnotherModel") - + This string can be: - + * RECURSIVE_RELATIONSHIP_CONSTANT (i.e. "self") to indicate a recursive relation. - + * The name of a model (i.e "AnotherModel") to indicate another model in the same app. - + * An app-label and model name (i.e. "someapp.AnotherModel") to indicate another model in a different app. - + If the other model hasn't yet been loaded -- almost a given if you're using lazy relationships -- then the relation won't be set up until the class_prepared signal fires at the end of model initialization. @@ -50,7 +51,7 @@ def add_lazy_relation(cls, field, relation): if relation == RECURSIVE_RELATIONSHIP_CONSTANT: app_label = cls._meta.app_label model_name = cls.__name__ - + else: # Look for an "app.Model" relation try: @@ -59,10 +60,10 @@ def add_lazy_relation(cls, field, relation): # If we can't split, assume a model in current app app_label = cls._meta.app_label model_name = relation - + # 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 - # model isn't loaded yet, so we need to pend the relation until the class + # model isn't loaded yet, so we need to pend the relation until the class # is prepared. model = get_model(app_label, model_name, False) if model: @@ -72,7 +73,7 @@ def add_lazy_relation(cls, field, relation): key = (app_label, model_name) value = (cls, field) pending_lookups.setdefault(key, []).append(value) - + def do_pending_lookups(sender): """ Handle any pending relations to the sending model. Sent from class_prepared. @@ -107,6 +108,8 @@ class RelatedField(object): add_lazy_relation(cls, self, other) else: self.do_related_class(other, cls) + if not cls._meta.abstract and self.rel.related_name: + self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()} def set_attributes_from_rel(self): self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name) @@ -136,6 +139,9 @@ class RelatedField(object): pass return v + if hasattr(value, 'as_sql'): + sql, params = value.as_sql() + return QueryWrapper(('(%s)' % sql), params) if lookup_type == 'exact': return [pk_trace(value)] if lookup_type == 'in': @@ -145,9 +151,10 @@ class RelatedField(object): raise TypeError, "Related Field has invalid lookup: %s" % lookup_type def _get_related_query_name(self, opts): - # This method defines the name that can be used to identify this related object - # in a table-spanning query. It uses the lower-cased object_name by default, - # but this can be overridden with the "related_name" option. + # This method defines the name that can be used to identify this + # related object in a table-spanning query. It uses the lower-cased + # object_name by default, but this can be overridden with the + # "related_name" option. return self.rel.related_name or opts.object_name.lower() class SingleRelatedObjectDescriptor(object): @@ -158,14 +165,19 @@ class SingleRelatedObjectDescriptor(object): # SingleRelatedObjectDescriptor instance. def __init__(self, related): self.related = related + self.cache_name = '_%s_cache' % related.field.name def __get__(self, instance, instance_type=None): if instance is None: raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name - params = {'%s__pk' % self.related.field.name: instance._get_pk_val()} - rel_obj = self.related.model._default_manager.get(**params) - return rel_obj + try: + return getattr(instance, self.cache_name) + except AttributeError: + params = {'%s__pk' % self.related.field.name: instance._get_pk_val()} + rel_obj = self.related.model._default_manager.get(**params) + setattr(instance, self.cache_name, rel_obj) + return rel_obj def __set__(self, instance, value): if instance is None: @@ -495,13 +507,77 @@ class ReverseManyRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) +class ManyToOneRel(object): + def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, + max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, + related_name=None, limit_choices_to=None, lookup_overrides=None, + raw_id_admin=False, parent_link=False): + try: + to._meta + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT + self.to, self.field_name = to, field_name + self.num_in_admin, self.edit_inline = num_in_admin, edit_inline + self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin + self.num_extra_on_change, self.related_name = num_extra_on_change, related_name + if limit_choices_to is None: + limit_choices_to = {} + self.limit_choices_to = limit_choices_to + self.lookup_overrides = lookup_overrides or {} + self.raw_id_admin = raw_id_admin + self.multiple = True + self.parent_link = parent_link + + def get_related_field(self): + """ + Returns the Field in the 'to' object to which this relationship is + tied. + """ + data = self.to._meta.get_field_by_name(self.field_name) + if not data[2]: + raise FieldDoesNotExist("No related field named '%s'" % + self.field_name) + return data[0] + +class OneToOneRel(ManyToOneRel): + def __init__(self, to, field_name, num_in_admin=0, min_num_in_admin=None, + max_num_in_admin=None, num_extra_on_change=None, edit_inline=False, + related_name=None, limit_choices_to=None, lookup_overrides=None, + raw_id_admin=False, parent_link=False): + # NOTE: *_num_in_admin and num_extra_on_change are intentionally + # ignored here. We accept them as parameters only to match the calling + # signature of ManyToOneRel.__init__(). + super(OneToOneRel, self).__init__(to, field_name, num_in_admin, + edit_inline=edit_inline, related_name=related_name, + limit_choices_to=limit_choices_to, + lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin, + parent_link=parent_link) + self.multiple = False + +class ManyToManyRel(object): + def __init__(self, to, num_in_admin=0, related_name=None, + filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): + self.to = to + self.num_in_admin = num_in_admin + self.related_name = related_name + self.filter_interface = filter_interface + if limit_choices_to is None: + limit_choices_to = {} + self.limit_choices_to = limit_choices_to + self.edit_inline = False + self.raw_id_admin = raw_id_admin + self.symmetrical = symmetrical + self.multiple = True + + assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" + class ForeignKey(RelatedField, Field): empty_strings_allowed = False - def __init__(self, to, to_field=None, **kwargs): + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: to_name = to._meta.object_name.lower() except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert isinstance(to, basestring), "ForeignKey(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (to, RECURSIVE_RELATIONSHIP_CONSTANT) + 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: to_field = to_field or to._meta.pk.name kwargs['verbose_name'] = kwargs.get('verbose_name', '') @@ -511,7 +587,7 @@ class ForeignKey(RelatedField, Field): warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.", DeprecationWarning) kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - kwargs['rel'] = ManyToOneRel(to, to_field, + kwargs['rel'] = rel_class(to, to_field, num_in_admin=kwargs.pop('num_in_admin', 3), min_num_in_admin=kwargs.pop('min_num_in_admin', None), max_num_in_admin=kwargs.pop('max_num_in_admin', None), @@ -520,7 +596,8 @@ class ForeignKey(RelatedField, Field): related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), lookup_overrides=kwargs.pop('lookup_overrides', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) + raw_id_admin=kwargs.pop('raw_id_admin', False), + parent_link=kwargs.pop('parent_link', False)) Field.__init__(self, **kwargs) self.db_index = True @@ -606,82 +683,25 @@ class ForeignKey(RelatedField, Field): return IntegerField().db_type() return rel_field.db_type() -class OneToOneField(RelatedField, IntegerField): +class OneToOneField(ForeignKey): + """ + A OneToOneField is essentially the same as a ForeignKey, with the exception + that always carries a "unique" constraint with it and the reverse relation + always returns the object pointed to (since there will only ever be one), + rather than returning a list. + """ def __init__(self, to, to_field=None, **kwargs): - try: - to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert isinstance(to, basestring), "OneToOneField(%r) is invalid. First parameter to OneToOneField must be either a model, a model name, or the string %r" % (to, RECURSIVE_RELATIONSHIP_CONSTANT) - else: - to_field = to_field or to._meta.pk.name - kwargs['verbose_name'] = kwargs.get('verbose_name', '') - - if 'edit_inline_type' in kwargs: - import warnings - warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.", DeprecationWarning) - kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - - kwargs['rel'] = OneToOneRel(to, to_field, - num_in_admin=kwargs.pop('num_in_admin', 0), - edit_inline=kwargs.pop('edit_inline', False), - related_name=kwargs.pop('related_name', None), - limit_choices_to=kwargs.pop('limit_choices_to', None), - lookup_overrides=kwargs.pop('lookup_overrides', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) - kwargs['primary_key'] = True - IntegerField.__init__(self, **kwargs) - - self.db_index = True - - def get_attname(self): - return '%s_id' % self.name - - def get_validator_unique_lookup_type(self): - return '%s__%s__exact' % (self.name, self.rel.get_related_field().name) - - # TODO: Copied from ForeignKey... putting this in RelatedField adversely affects - # ManyToManyField. This works for now. - def prepare_field_objs_and_params(self, manipulator, name_prefix): - params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname} - if self.rel.raw_id_admin: - field_objs = self.get_manipulator_field_objs() - params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) - else: - if self.radio_admin: - field_objs = [oldforms.RadioSelectField] - params['ul_class'] = get_ul_class(self.radio_admin) - else: - if self.null: - field_objs = [oldforms.NullSelectField] - else: - field_objs = [oldforms.SelectField] - params['choices'] = self.get_choices_default() - return field_objs, params - - def contribute_to_class(self, cls, name): - super(OneToOneField, self).contribute_to_class(cls, name) - setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self)) + kwargs['unique'] = True + if 'num_in_admin' not in kwargs: + kwargs['num_in_admin'] = 0 + super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) def contribute_to_related_class(self, cls, related): - setattr(cls, related.get_accessor_name(), SingleRelatedObjectDescriptor(related)) + setattr(cls, related.get_accessor_name(), + SingleRelatedObjectDescriptor(related)) if not cls._meta.one_to_one_field: cls._meta.one_to_one_field = self - def formfield(self, **kwargs): - defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.all()} - defaults.update(kwargs) - return super(OneToOneField, self).formfield(**defaults) - - def db_type(self): - # The database column type of a OneToOneField is the column type - # of the field to which it points. An exception is if the OneToOneField - # points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField, - # in which case the column type is simply that of an IntegerField. - rel_field = self.rel.get_related_field() - if isinstance(rel_field, (AutoField, PositiveIntegerField, PositiveSmallIntegerField)): - return IntegerField().db_type() - return rel_field.db_type() - class ManyToManyField(RelatedField, Field): def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', None) @@ -801,7 +821,7 @@ class ManyToManyField(RelatedField, Field): def save_form_data(self, instance, data): setattr(instance, self.attname, data) - + def formfield(self, **kwargs): defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()} defaults.update(kwargs) @@ -816,56 +836,3 @@ class ManyToManyField(RelatedField, Field): # so return None. return None -class ManyToOneRel(object): - def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, - max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False): - try: - to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT - self.to, self.field_name = to, field_name - self.num_in_admin, self.edit_inline = num_in_admin, edit_inline - self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin - self.num_extra_on_change, self.related_name = num_extra_on_change, related_name - if limit_choices_to is None: - limit_choices_to = {} - self.limit_choices_to = limit_choices_to - self.lookup_overrides = lookup_overrides or {} - self.raw_id_admin = raw_id_admin - self.multiple = True - - def get_related_field(self): - "Returns the Field in the 'to' object to which this relationship is tied." - return self.to._meta.get_field(self.field_name) - -class OneToOneRel(ManyToOneRel): - def __init__(self, to, field_name, num_in_admin=0, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, - raw_id_admin=False): - self.to, self.field_name = to, field_name - self.num_in_admin, self.edit_inline = num_in_admin, edit_inline - self.related_name = related_name - if limit_choices_to is None: - limit_choices_to = {} - self.limit_choices_to = limit_choices_to - self.lookup_overrides = lookup_overrides or {} - self.raw_id_admin = raw_id_admin - self.multiple = False - -class ManyToManyRel(object): - def __init__(self, to, num_in_admin=0, related_name=None, - filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): - self.to = to - self.num_in_admin = num_in_admin - self.related_name = related_name - self.filter_interface = filter_interface - if limit_choices_to is None: - limit_choices_to = {} - self.limit_choices_to = limit_choices_to - self.edit_inline = False - self.raw_id_admin = raw_id_admin - self.symmetrical = symmetrical - self.multiple = True - - assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 7b2e916738..3a9da34a49 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,11 +1,13 @@ -from django.db.models.query import QuerySet, EmptyQuerySet +import copy + +from django.db.models.query import QuerySet, EmptyQuerySet, insert_query from django.dispatch import dispatcher from django.db.models import signals from django.db.models.fields import FieldDoesNotExist def ensure_default_manager(sender): cls = sender - if not hasattr(cls, '_default_manager'): + if not getattr(cls, '_default_manager', None) and not cls._meta.abstract: # Create the default manager, if needed. try: cls._meta.get_field('objects') @@ -31,13 +33,24 @@ class Manager(object): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model setattr(model, name, ManagerDescriptor(self)) - if not hasattr(model, '_default_manager') or self.creation_counter < model._default_manager.creation_counter: + if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter: model._default_manager = self + def _copy_to_model(self, model): + """ + Makes a copy of the manager and assigns it to 'model', which should be + a child of the existing model (used when inheriting a manager from an + abstract base class). + """ + assert issubclass(model, self.model) + mgr = copy.copy(self) + mgr.model = model + return mgr + ####################### # PROXIES TO QUERYSET # ####################### - + def get_empty_query_set(self): return EmptyQuerySet(self.model) @@ -46,7 +59,7 @@ class Manager(object): to easily customize the behavior of the Manager. """ return QuerySet(self.model) - + def none(self): return self.get_empty_query_set() @@ -70,7 +83,7 @@ class Manager(object): def get_or_create(self, **kwargs): return self.get_query_set().get_or_create(**kwargs) - + def create(self, **kwargs): return self.get_query_set().create(**kwargs) @@ -101,6 +114,21 @@ class Manager(object): def values(self, *args, **kwargs): return self.get_query_set().values(*args, **kwargs) + def values_list(self, *args, **kwargs): + return self.get_query_set().values_list(*args, **kwargs) + + def update(self, *args, **kwargs): + return self.get_query_set().update(*args, **kwargs) + + def reverse(self, *args, **kwargs): + return self.get_query_set().reverse(*args, **kwargs) + + def _insert(self, values, **kwargs): + return insert_query(self.model, values, **kwargs) + + def _update(self, values, **kwargs): + return self.get_query_set()._update(values, **kwargs) + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. # For example, Poll.objects works, but poll_obj.objects raises AttributeError. diff --git a/django/db/models/options.py b/django/db/models/options.py index 37ace0a7c1..5802ead081 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,25 +1,32 @@ +import re +from bisect import bisect +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + from django.conf import settings from django.db.models.related import RelatedObject from django.db.models.fields.related import ManyToManyRel from django.db.models.fields import AutoField, FieldDoesNotExist +from django.db.models.fields.proxy import OrderWrt from django.db.models.loading import get_models, app_cache_ready -from django.db.models.query import orderlist2sql from django.db.models import Manager from django.utils.translation import activate, deactivate_all, get_language, string_concat from django.utils.encoding import force_unicode, smart_str -from bisect import bisect -import re +from django.utils.datastructures import SortedDict # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip() DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', - 'order_with_respect_to', 'app_label', 'db_tablespace') + 'order_with_respect_to', 'app_label', 'db_tablespace', + 'abstract') class Options(object): def __init__(self, meta): - self.fields, self.many_to_many = [], [] + self.local_fields, self.local_many_to_many = [], [] self.module_name, self.verbose_name = None, None self.verbose_name_plural = None self.db_table = '' @@ -35,7 +42,8 @@ class Options(object): self.pk = None self.has_auto_field, self.auto_field = False, None self.one_to_one_field = None - self.parents = [] + self.abstract = False + self.parents = SortedDict() def contribute_to_class(self, cls, name): cls._meta = self @@ -47,11 +55,14 @@ class Options(object): # Next, apply any overridden values from 'class Meta'. if self.meta: - meta_attrs = self.meta.__dict__ + meta_attrs = self.meta.__dict__.copy() del meta_attrs['__module__'] del meta_attrs['__doc__'] for attr_name in DEFAULT_NAMES: - setattr(self, attr_name, meta_attrs.pop(attr_name, getattr(self, attr_name))) + if attr_name in meta_attrs: + setattr(self, attr_name, meta_attrs.pop(attr_name)) + elif hasattr(self.meta, attr_name): + setattr(self, attr_name, getattr(self.meta, attr_name)) # unique_together can be either a tuple of tuples, or a single # tuple of two strings. Normalize it to a tuple of tuples, so that @@ -82,9 +93,16 @@ class Options(object): self.order_with_respect_to = None if self.pk is None: - auto = AutoField(verbose_name='ID', primary_key=True) - auto.creation_counter = -1 - model.add_to_class('id', auto) + if self.parents: + # Promote the first parent link in lieu of adding yet another + # field. + field = self.parents.value_for_index(0) + field.primary_key = True + self.pk = field + else: + auto = AutoField(verbose_name='ID', primary_key=True, + auto_created=True) + model.add_to_class('id', auto) # If the db_table wasn't provided, use the app_label + module_name. if not self.db_table: @@ -94,14 +112,26 @@ class Options(object): def add_field(self, field): # Insert the given field in the order in which it was created, using # the "creation_counter" attribute of the field. - # Move many-to-many related fields from self.fields into self.many_to_many. + # Move many-to-many related fields from self.fields into + # self.many_to_many. if field.rel and isinstance(field.rel, ManyToManyRel): - self.many_to_many.insert(bisect(self.many_to_many, field), field) + self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field) + if hasattr(self, '_m2m_cache'): + del self._m2m_cache else: - self.fields.insert(bisect(self.fields, field), field) - if not self.pk and field.primary_key: - self.pk = field - field.serialize = False + self.local_fields.insert(bisect(self.local_fields, field), field) + self.setup_pk(field) + if hasattr(self, '_field_cache'): + del self._field_cache + del self._field_name_cache + + if hasattr(self, '_name_map'): + del self._name_map + + def setup_pk(self, field): + if not self.pk and field.primary_key: + self.pk = field + field.serialize = False def __repr__(self): return '' % self.object_name @@ -122,19 +152,137 @@ class Options(object): return raw verbose_name_raw = property(verbose_name_raw) + def _fields(self): + """ + The getter for self.fields. This returns the list of field objects + available to this model (including through parent models). + + Callers are not permitted to modify this list, since it's a reference + to this instance (not a copy). + """ + try: + self._field_name_cache + except AttributeError: + self._fill_fields_cache() + return self._field_name_cache + fields = property(_fields) + + def get_fields_with_model(self): + """ + Returns a sequence of (field, model) pairs for all fields. The "model" + element is None for fields on the current model. Mostly of use when + constructing queries so that we know which model a field belongs to. + """ + try: + self._field_cache + except AttributeError: + self._fill_fields_cache() + return self._field_cache + + def _fill_fields_cache(self): + cache = [] + for parent in self.parents: + for field, model in parent._meta.get_fields_with_model(): + if model: + cache.append((field, model)) + else: + cache.append((field, parent)) + cache.extend([(f, None) for f in self.local_fields]) + self._field_cache = tuple(cache) + self._field_name_cache = [x for x, _ in cache] + + def _many_to_many(self): + try: + self._m2m_cache + except AttributeError: + self._fill_m2m_cache() + return self._m2m_cache.keys() + many_to_many = property(_many_to_many) + + def get_m2m_with_model(self): + """ + The many-to-many version of get_fields_with_model(). + """ + try: + self._m2m_cache + except AttributeError: + self._fill_m2m_cache() + return self._m2m_cache.items() + + def _fill_m2m_cache(self): + cache = SortedDict() + for parent in self.parents: + for field, model in parent._meta.get_m2m_with_model(): + if model: + cache[field] = model + else: + cache[field] = parent + for field in self.local_many_to_many: + cache[field] = None + self._m2m_cache = cache + def get_field(self, name, many_to_many=True): - "Returns the requested field by name. Raises FieldDoesNotExist on error." + """ + Returns the requested field by name. Raises FieldDoesNotExist on error. + """ to_search = many_to_many and (self.fields + self.many_to_many) or self.fields for f in to_search: if f.name == name: return f raise FieldDoesNotExist, '%s has no field named %r' % (self.object_name, name) - def get_order_sql(self, table_prefix=''): - "Returns the full 'ORDER BY' clause for this object, according to self.ordering." - if not self.ordering: return '' - pre = table_prefix and (table_prefix + '.') or '' - return 'ORDER BY ' + orderlist2sql(self.ordering, self, pre) + def get_field_by_name(self, name): + """ + Returns the (field_object, model, direct, m2m), where field_object is + the Field instance for the given name, model is the model containing + this field (None for local fields), direct is True if the field exists + on this model, and m2m is True for many-to-many relations. When + 'direct' is False, 'field_object' is the corresponding RelatedObject + for this field (since the field doesn't have an instance associated + with it). + + Uses a cache internally, so after the first access, this is very fast. + """ + try: + try: + return self._name_map[name] + except AttributeError: + cache = self.init_name_map() + return cache[name] + except KeyError: + raise FieldDoesNotExist('%s has no field named %r' + % (self.object_name, name)) + + def get_all_field_names(self): + """ + Returns a list of all field names that are possible for this model + (including reverse relation names). + """ + try: + cache = self._name_map + except AttributeError: + cache = self.init_name_map() + names = cache.keys() + names.sort() + return names + + def init_name_map(self): + """ + Initialises the field name -> field object mapping. + """ + cache = dict([(f.name, (f, m, True, False)) for f, m in + self.get_fields_with_model()]) + for f, model in self.get_m2m_with_model(): + cache[f.name] = (f, model, True, True) + for f, model in self.get_all_related_m2m_objects_with_model(): + cache[f.field.related_query_name()] = (f, model, False, True) + for f, model in self.get_all_related_objects_with_model(): + cache[f.field.related_query_name()] = (f, model, False, False) + if self.order_with_respect_to: + cache['_order'] = OrderWrt(), None, True, False + if app_cache_ready(): + self._name_map = cache + return cache def get_add_permission(self): return 'add_%s' % self.object_name.lower() @@ -145,17 +293,81 @@ class Options(object): def get_delete_permission(self): return 'delete_%s' % self.object_name.lower() - def get_all_related_objects(self): - try: # Try the cache first. - return self._all_related_objects + def get_all_related_objects(self, local_only=False): + try: + self._related_objects_cache except AttributeError: - rel_objs = [] - for klass in get_models(): - for f in klass._meta.fields: - if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta: - rel_objs.append(RelatedObject(f.rel.to, klass, f)) - self._all_related_objects = rel_objs - return rel_objs + self._fill_related_objects_cache() + if local_only: + return [k for k, v in self._related_objects_cache.items() if not v] + return self._related_objects_cache.keys() + + def get_all_related_objects_with_model(self): + """ + Returns a list of (related-object, model) pairs. Similar to + get_fields_with_model(). + """ + try: + self._related_objects_cache + except AttributeError: + self._fill_related_objects_cache() + return self._related_objects_cache.items() + + def _fill_related_objects_cache(self): + cache = SortedDict() + parent_list = self.get_parent_list() + for parent in self.parents: + for obj, model in parent._meta.get_all_related_objects_with_model(): + if (obj.field.creation_counter < 0 or obj.field.rel.parent_link) and obj.model not in parent_list: + continue + if not model: + cache[obj] = parent + else: + cache[obj] = model + for klass in get_models(): + for f in klass._meta.local_fields: + if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta: + cache[RelatedObject(f.rel.to, klass, f)] = None + self._related_objects_cache = cache + + def get_all_related_many_to_many_objects(self, local_only=False): + try: + cache = self._related_many_to_many_cache + except AttributeError: + cache = self._fill_related_many_to_many_cache() + if local_only: + return [k for k, v in cache.items() if not v] + return cache.keys() + + def get_all_related_m2m_objects_with_model(self): + """ + Returns a list of (related-m2m-object, model) pairs. Similar to + get_fields_with_model(). + """ + try: + cache = self._related_many_to_many_cache + except AttributeError: + cache = self._fill_related_many_to_many_cache() + return cache.items() + + def _fill_related_many_to_many_cache(self): + cache = SortedDict() + parent_list = self.get_parent_list() + for parent in self.parents: + for obj, model in parent._meta.get_all_related_m2m_objects_with_model(): + if obj.field.creation_counter < 0 and obj.model not in parent_list: + continue + if not model: + cache[obj] = parent + else: + cache[obj] = model + for klass in get_models(): + for f in klass._meta.local_many_to_many: + if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta: + cache[RelatedObject(f.rel.to, klass, f)] = None + if app_cache_ready(): + self._related_many_to_many_cache = cache + return cache def get_followed_related_objects(self, follow=None): if follow == None: @@ -179,18 +391,34 @@ class Options(object): follow[f.name] = fol return follow - def get_all_related_many_to_many_objects(self): - try: # Try the cache first. - return self._all_related_many_to_many_objects - except AttributeError: - rel_objs = [] - for klass in get_models(): - for f in klass._meta.many_to_many: - if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta: - rel_objs.append(RelatedObject(f.rel.to, klass, f)) - if app_cache_ready(): - self._all_related_many_to_many_objects = rel_objs - return rel_objs + def get_base_chain(self, model): + """ + Returns a list of parent classes leading to 'model' (order from closet + to most distant ancestor). This has to handle the case were 'model' is + a granparent or even more distant relation. + """ + if not self.parents: + return + if model in self.parents: + return [model] + for parent in self.parents: + res = parent._meta.get_base_chain(model) + if res: + res.insert(0, parent) + return res + raise TypeError('%r is not an ancestor of this model' + % model._meta.module_name) + + def get_parent_list(self): + """ + Returns a list of all the ancestor of this model as a list. Useful for + determining if something is an ancestor, regardless of lineage. + """ + result = set() + for parent in self.parents: + result.add(parent) + result.update(parent._meta.get_parent_list()) + return result def get_ordered_objects(self): "Returns a list of Options objects that are ordered with respect to this object." diff --git a/django/db/models/query.py b/django/db/models/query.py index a3d00c2ead..e1e2bb19f2 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,173 +1,134 @@ +import warnings + from django.conf import settings from django.db import connection, transaction, IntegrityError from django.db.models.fields import DateField, FieldDoesNotExist -from django.db.models import signals, loading +from django.db.models.query_utils import Q +from django.db.models import signals, sql from django.dispatch import dispatcher from django.utils.datastructures import SortedDict -from django.utils.encoding import smart_unicode -from django.contrib.contenttypes import generic -import datetime -import operator -import re -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback +# Used to control how many objects are worked with at once in some cases (e.g. +# when deleting objects). +CHUNK_SIZE = 100 +ITER_CHUNK_SIZE = CHUNK_SIZE -# The string constant used to separate query parts -LOOKUP_SEPARATOR = '__' +# Pull into this namespace for backwards compatibility +EmptyResultSet = sql.EmptyResultSet -# The list of valid query types -QUERY_TERMS = ( - 'exact', 'iexact', 'contains', 'icontains', - 'gt', 'gte', 'lt', 'lte', 'in', - 'startswith', 'istartswith', 'endswith', 'iendswith', - 'range', 'year', 'month', 'day', 'isnull', 'search', - 'regex', 'iregex', -) - -# Size of each "chunk" for get_iterator calls. -# Larger values are slightly faster at the expense of more storage space. -GET_ITERATOR_CHUNK_SIZE = 100 - -class EmptyResultSet(Exception): - pass - -#################### -# HELPER FUNCTIONS # -#################### - -# Django currently supports two forms of ordering. -# Form 1 (deprecated) example: -# order_by=(('pub_date', 'DESC'), ('headline', 'ASC'), (None, 'RANDOM')) -# Form 2 (new-style) example: -# order_by=('-pub_date', 'headline', '?') -# Form 1 is deprecated and will no longer be supported for Django's first -# official release. The following code converts from Form 1 to Form 2. - -LEGACY_ORDERING_MAPPING = {'ASC': '_', 'DESC': '-_', 'RANDOM': '?'} - -def handle_legacy_orderlist(order_list): - if not order_list or isinstance(order_list[0], basestring): - return order_list - else: - import warnings - new_order_list = [LEGACY_ORDERING_MAPPING[j.upper()].replace('_', smart_unicode(i)) for i, j in order_list] - warnings.warn("%r ordering syntax is deprecated. Use %r instead." % (order_list, new_order_list), DeprecationWarning) - return new_order_list - -def orderfield2column(f, opts): - try: - return opts.get_field(f, False).column - except FieldDoesNotExist: - return f - -def orderlist2sql(order_list, opts, prefix=''): - qn = connection.ops.quote_name - if prefix.endswith('.'): - prefix = qn(prefix[:-1]) + '.' - output = [] - for f in handle_legacy_orderlist(order_list): - if f.startswith('-'): - output.append('%s%s DESC' % (prefix, qn(orderfield2column(f[1:], opts)))) - elif f == '?': - output.append(connection.ops.random_function_sql()) - else: - output.append('%s%s ASC' % (prefix, qn(orderfield2column(f, opts)))) - return ', '.join(output) - -def quote_only_if_word(word): - if re.search('\W', word): # Don't quote if there are spaces or non-word chars. - return word - else: - return connection.ops.quote_name(word) - -class _QuerySet(object): +class QuerySet(object): "Represents a lazy database lookup for a set of objects" - def __init__(self, model=None): + def __init__(self, model=None, query=None): self.model = model - self._filters = Q() - self._order_by = None # Ordering, e.g. ('date', '-name'). If None, use model's ordering. - self._select_related = False # Whether to fill cache for related objects. - self._max_related_depth = 0 # Maximum "depth" for select_related - self._distinct = False # Whether the query should use SELECT DISTINCT. - self._select = {} # Dictionary of attname -> SQL. - self._where = [] # List of extra WHERE clauses to use. - self._params = [] # List of params to use for extra WHERE clauses. - self._tables = [] # List of extra tables to use. - self._offset = None # OFFSET clause. - self._limit = None # LIMIT clause. + self.query = query or sql.Query(self.model, connection) self._result_cache = None + self._iter = None ######################## # PYTHON MAGIC METHODS # ######################## def __repr__(self): - return repr(self._get_data()) + return repr(list(self)) def __len__(self): - return len(self._get_data()) + # Since __len__ is called quite frequently (for example, as part of + # list(qs), we make some effort here to be as efficient as possible + # whilst not messing up any existing iterators against the queryset. + if self._result_cache is None: + if self._iter: + self._result_cache = list(self._iter()) + else: + self._result_cache = list(self.iterator()) + elif self._iter: + self._result_cache.extend(list(self._iter)) + return len(self._result_cache) def __iter__(self): - return iter(self._get_data()) + if self._result_cache is None: + self._iter = self.iterator() + self._result_cache = [] + if self._iter: + return self._result_iter() + # Python's list iterator is better than our version when we're just + # iterating over the cache. + return iter(self._result_cache) + + def _result_iter(self): + pos = 0 + while 1: + upper = len(self._result_cache) + while pos < upper: + yield self._result_cache[pos] + pos = pos + 1 + if not self._iter: + raise StopIteration + if len(self._result_cache) <= pos: + self._fill_cache() + + def __nonzero__(self): + if self._result_cache is not None: + return bool(self._result_cache) + try: + iter(self).next() + except StopIteration: + return False + return True def __getitem__(self, k): "Retrieve an item or slice from the set of results." if not isinstance(k, (slice, int, long)): raise TypeError - assert (not isinstance(k, slice) and (k >= 0)) \ - or (isinstance(k, slice) and (k.start is None or k.start >= 0) and (k.stop is None or k.stop >= 0)), \ - "Negative indexing is not supported." - if self._result_cache is None: - if isinstance(k, slice): - # Offset: - if self._offset is None: - offset = k.start - elif k.start is None: - offset = self._offset - else: - offset = self._offset + k.start - # Now adjust offset to the bounds of any existing limit: - if self._limit is not None and k.start is not None: - limit = self._limit - k.start - else: - limit = self._limit + assert ((not isinstance(k, slice) and (k >= 0)) + or (isinstance(k, slice) and (k.start is None or k.start >= 0) + and (k.stop is None or k.stop >= 0))), \ + "Negative indexing is not supported." - # Limit: - if k.stop is not None and k.start is not None: - if limit is None: - limit = k.stop - k.start + if self._result_cache is not None: + if self._iter is not None: + # The result cache has only been partially populated, so we may + # need to fill it out a bit more. + if isinstance(k, slice): + if k.stop is not None: + # Some people insist on passing in strings here. + bound = int(k.stop) else: - limit = min((k.stop - k.start), limit) + bound = None else: - if limit is None: - limit = k.stop - else: - if k.stop is not None: - limit = min(k.stop, limit) - - if k.step is None: - return self._clone(_offset=offset, _limit=limit) - else: - return list(self._clone(_offset=offset, _limit=limit))[::k.step] - else: - try: - return list(self._clone(_offset=k, _limit=1))[0] - except self.model.DoesNotExist, e: - raise IndexError, e.args - else: + bound = k + 1 + if len(self._result_cache) < bound: + self._fill_cache(bound - len(self._result_cache)) return self._result_cache[k] + if isinstance(k, slice): + qs = self._clone() + if k.start is not None: + start = int(k.start) + else: + start = None + if k.stop is not None: + stop = int(k.stop) + else: + stop = None + qs.query.set_limits(start, stop) + return k.step and list(qs)[::k.step] or qs + try: + qs = self._clone() + qs.query.set_limits(k, k + 1) + return list(qs)[0] + except self.model.DoesNotExist, e: + raise IndexError, e.args + def __and__(self, other): - combined = self._combine(other) - combined._filters = self._filters & other._filters + self._merge_sanity_check(other) + combined = self._clone() + combined.query.combine(other.query, sql.AND) return combined def __or__(self, other): - combined = self._combine(other) - combined._filters = self._filters | other._filters + self._merge_sanity_check(other) + combined = self._clone() + combined.query.combine(other.query, sql.OR) return combined #################################### @@ -175,38 +136,27 @@ class _QuerySet(object): #################################### def iterator(self): - "Performs the SELECT database lookup of this QuerySet." - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - raise StopIteration - - # self._select is a dictionary, and dictionaries' key order is - # undefined, so we convert it to a list of tuples. - extra_select = self._select.items() - - cursor = connection.cursor() - cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) - - fill_cache = self._select_related - fields = self.model._meta.fields - index_end = len(fields) - has_resolve_columns = hasattr(self, 'resolve_columns') - while 1: - rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) - if not rows: - raise StopIteration - for row in rows: - if has_resolve_columns: - row = self.resolve_columns(row, fields) - if fill_cache: - obj, index_end = get_cached_row(klass=self.model, row=row, - index_start=0, max_depth=self._max_related_depth) - else: - obj = self.model(*row[:index_end]) - for i, k in enumerate(extra_select): - setattr(obj, k[0], row[index_end+i]) - yield obj + """ + An iterator over the results from applying this QuerySet to the + database. + """ + fill_cache = self.query.select_related + if isinstance(fill_cache, dict): + requested = fill_cache + else: + requested = None + max_depth = self.query.max_depth + extra_select = self.query.extra_select.keys() + index_start = len(extra_select) + for row in self.query.results_iter(): + if fill_cache: + obj, _ = get_cached_row(self.model, row, index_start, + max_depth, requested=requested) + else: + obj = self.model.from_sequence(row[index_start:]) + for i, k in enumerate(extra_select): + setattr(obj, k, row[i]) + yield obj def count(self): """ @@ -220,50 +170,22 @@ class _QuerySet(object): if self._result_cache is not None: return len(self._result_cache) - counter = self._clone() - counter._order_by = () - counter._select_related = False - - offset = counter._offset - limit = counter._limit - counter._offset = None - counter._limit = None - - try: - select, sql, params = counter._get_sql_clause() - except EmptyResultSet: - return 0 - - cursor = connection.cursor() - if self._distinct: - id_col = "%s.%s" % (connection.ops.quote_name(self.model._meta.db_table), - connection.ops.quote_name(self.model._meta.pk.column)) - cursor.execute("SELECT COUNT(DISTINCT(%s))" % id_col + sql, params) - else: - cursor.execute("SELECT COUNT(*)" + sql, params) - count = cursor.fetchone()[0] - - # Apply any offset and limit constraints manually, since using LIMIT or - # OFFSET in SQL doesn't change the output of COUNT. - if offset: - count = max(0, count - offset) - if limit: - count = min(limit, count) - - return count + return self.query.get_count() def get(self, *args, **kwargs): - "Performs the SELECT and returns a single object matching the given keyword arguments." + """ + Performs the query and returns a single object matching the given + keyword arguments. + """ clone = self.filter(*args, **kwargs) - # clean up SQL by removing unneeded ORDER BY - if not clone._order_by: - clone._order_by = () - obj_list = list(clone) - if len(obj_list) < 1: - raise self.model.DoesNotExist, "%s matching query does not exist." % self.model._meta.object_name - elif len(obj_list) > 1: - raise self.model.MultipleObjectsReturned, "get() returned more than one %s -- it returned %s! Lookup parameters were %s" % (self.model._meta.object_name, len(obj_list), kwargs) - return obj_list[0] + num = len(clone) + if num == 1: + return clone._result_cache[0] + if not num: + raise self.model.DoesNotExist("%s matching query does not exist." + % self.model._meta.object_name) + raise self.model.MultipleObjectsReturned("get() returned more than one %s -- it returned %s! Lookup parameters were %s" + % (self.model._meta.object_name, num, kwargs)) def create(self, **kwargs): """ @@ -280,7 +202,8 @@ class _QuerySet(object): Returns a tuple of (object, created), where created is a boolean specifying whether an object was created. """ - assert len(kwargs), 'get_or_create() must be passed at least one keyword argument' + assert kwargs, \ + 'get_or_create() must be passed at least one keyword argument' defaults = kwargs.pop('defaults', {}) try: return self.get(**kwargs), False @@ -301,400 +224,384 @@ class _QuerySet(object): """ latest_by = field_name or self.model._meta.get_latest_by assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model" - assert self._limit is None and self._offset is None, \ + assert self.query.can_filter(), \ "Cannot change a query once a slice has been taken." - return self._clone(_limit=1, _order_by=('-'+latest_by,)).get() + obj = self._clone() + obj.query.set_limits(high=1) + obj.query.add_ordering('-%s' % latest_by) + return obj.get() def in_bulk(self, id_list): """ Returns a dictionary mapping each of the given IDs to the object with that ID. """ - assert self._limit is None and self._offset is None, \ + assert self.query.can_filter(), \ "Cannot use 'limit' or 'offset' with in_bulk" - assert isinstance(id_list, (tuple, list)), "in_bulk() must be provided with a list of IDs." - qn = connection.ops.quote_name - id_list = list(id_list) - if id_list == []: + assert isinstance(id_list, (tuple, list)), \ + "in_bulk() must be provided with a list of IDs." + if not id_list: return {} qs = self._clone() - qs._where.append("%s.%s IN (%s)" % (qn(self.model._meta.db_table), qn(self.model._meta.pk.column), ",".join(['%s'] * len(id_list)))) - qs._params.extend(id_list) + qs.query.add_filter(('pk__in', id_list)) return dict([(obj._get_pk_val(), obj) for obj in qs.iterator()]) def delete(self): """ Deletes the records in the current QuerySet. """ - assert self._limit is None and self._offset is None, \ - "Cannot use 'limit' or 'offset' with delete." + assert self.query.can_filter(), \ + "Cannot use 'limit' or 'offset' with delete." del_query = self._clone() - # disable non-supported fields - del_query._select_related = False - del_query._order_by = [] + # Disable non-supported fields. + del_query.query.select_related = False + del_query.query.clear_ordering() - # Delete objects in chunks to prevent an the list of - # related objects from becoming too long - more_objects = True - while more_objects: - # Collect all the objects to be deleted in this chunk, and all the objects - # that are related to the objects that are to be deleted + # Delete objects in chunks to prevent the list of related objects from + # becoming too long. + while 1: + # Collect all the objects to be deleted in this chunk, and all the + # objects that are related to the objects that are to be deleted. seen_objs = SortedDict() - more_objects = False - for object in del_query[0:GET_ITERATOR_CHUNK_SIZE]: - more_objects = True + for object in del_query[:CHUNK_SIZE]: object._collect_sub_objects(seen_objs) - # If one or more objects were found, delete them. - # Otherwise, stop looping. - if more_objects: - delete_objects(seen_objs) + if not seen_objs: + break + delete_objects(seen_objs) # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None delete.alters_data = True + def update(self, **kwargs): + """ + Updates all elements in the current QuerySet, setting all the given + fields to the appropriate values. + """ + query = self.query.clone(sql.UpdateQuery) + query.add_update_values(kwargs) + query.execute_sql(None) + self._result_cache = None + update.alters_data = True + + def _update(self, values): + """ + A version of update that accepts field objects instead of field names. + Used primarily for model saving and not intended for use by general + code (it requires too much poking around at model internals to be + useful at that level). + """ + query = self.query.clone(sql.UpdateQuery) + query.add_update_fields(values) + query.execute_sql(None) + self._result_cache = None + _update.alters_data = True + ################################################## # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # ################################################## def values(self, *fields): - return self._clone(klass=ValuesQuerySet, _fields=fields) + return self._clone(klass=ValuesQuerySet, setup=True, _fields=fields) + + def values_list(self, *fields, **kwargs): + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' + % (kwargs.keys(),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is called with more than one field.") + return self._clone(klass=ValuesListQuerySet, setup=True, flat=flat, + _fields=fields) def dates(self, field_name, kind, order='ASC'): """ Returns a list of datetime objects representing all available dates for the given field_name, scoped to 'kind'. """ - assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'." - assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'." + assert kind in ("month", "year", "day"), \ + "'kind' must be one of 'year', 'month' or 'day'." + assert order in ('ASC', 'DESC'), \ + "'order' must be either 'ASC' or 'DESC'." # Let the FieldDoesNotExist exception propagate. field = self.model._meta.get_field(field_name, many_to_many=False) - assert isinstance(field, DateField), "%r isn't a DateField." % field_name - return self._clone(klass=DateQuerySet, _field=field, _kind=kind, _order=order) + assert isinstance(field, DateField), "%r isn't a DateField." \ + % field_name + return self._clone(klass=DateQuerySet, setup=True, _field=field, + _kind=kind, _order=order) + + def none(self): + """ + Returns an empty queryset. + """ + return self._clone(klass=EmptyQuerySet) ################################################################## # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET # ################################################################## + def all(self): + """ + Returns a new QuerySet that is a copy of the current one. This allows a + QuerySet to proxy for a model manager in some cases. + """ + return self._clone() + def filter(self, *args, **kwargs): - "Returns a new QuerySet instance with the args ANDed to the existing set." - return self._filter_or_exclude(None, *args, **kwargs) + """ + Returns a new QuerySet instance with the args ANDed to the existing + set. + """ + return self._filter_or_exclude(False, *args, **kwargs) def exclude(self, *args, **kwargs): - "Returns a new QuerySet instance with NOT (args) ANDed to the existing set." - return self._filter_or_exclude(QNot, *args, **kwargs) + """ + Returns a new QuerySet instance with NOT (args) ANDed to the existing + set. + """ + return self._filter_or_exclude(True, *args, **kwargs) - def _filter_or_exclude(self, mapper, *args, **kwargs): - # mapper is a callable used to transform Q objects, - # or None for identity transform - if mapper is None: - mapper = lambda x: x - if len(args) > 0 or len(kwargs) > 0: - assert self._limit is None and self._offset is None, \ - "Cannot filter a query once a slice has been taken." + def _filter_or_exclude(self, negate, *args, **kwargs): + if args or kwargs: + assert self.query.can_filter(), \ + "Cannot filter a query once a slice has been taken." clone = self._clone() - if len(kwargs) > 0: - clone._filters = clone._filters & mapper(Q(**kwargs)) - if len(args) > 0: - clone._filters = clone._filters & reduce(operator.and_, map(mapper, args)) + if negate: + clone.query.add_q(~Q(*args, **kwargs)) + else: + clone.query.add_q(Q(*args, **kwargs)) return clone def complex_filter(self, filter_obj): - """Returns a new QuerySet instance with filter_obj added to the filters. - filter_obj can be a Q object (has 'get_sql' method) or a dictionary of - keyword lookup arguments.""" - # This exists to support framework features such as 'limit_choices_to', - # and usually it will be more natural to use other methods. - if hasattr(filter_obj, 'get_sql'): + """ + Returns a new QuerySet instance with filter_obj added to the filters. + filter_obj can be a Q object (or anything with an add_to_query() + method) or a dictionary of keyword lookup arguments. + + This exists to support framework features such as 'limit_choices_to', + and usually it will be more natural to use other methods. + """ + if isinstance(filter_obj, Q) or hasattr(filter_obj, 'add_to_query'): return self._filter_or_exclude(None, filter_obj) else: return self._filter_or_exclude(None, **filter_obj) - def select_related(self, true_or_false=True, depth=0): - "Returns a new QuerySet instance with '_select_related' modified." - return self._clone(_select_related=true_or_false, _max_related_depth=depth) + def select_related(self, *fields, **kwargs): + """ + Returns a new QuerySet instance that will select related objects. If + fields are specified, they must be ForeignKey fields and only those + related objects are included in the selection. + """ + depth = kwargs.pop('depth', 0) + if kwargs: + raise TypeError('Unexpected keyword arguments to select_related: %s' + % (kwargs.keys(),)) + obj = self._clone() + if fields: + if depth: + raise TypeError('Cannot pass both "depth" and fields to select_related()') + obj.query.add_select_related(fields) + else: + obj.query.select_related = True + if depth: + obj.query.max_depth = depth + return obj + + def dup_select_related(self, other): + """ + Copies the related selection status from the queryset 'other' to the + current queryset. + """ + self.query.select_related = other.query.select_related def order_by(self, *field_names): - "Returns a new QuerySet instance with the ordering changed." - assert self._limit is None and self._offset is None, \ + """Returns a new QuerySet instance with the ordering changed.""" + assert self.query.can_filter(), \ "Cannot reorder a query once a slice has been taken." - return self._clone(_order_by=field_names) + obj = self._clone() + obj.query.clear_ordering() + obj.query.add_ordering(*field_names) + return obj def distinct(self, true_or_false=True): - "Returns a new QuerySet instance with '_distinct' modified." - return self._clone(_distinct=true_or_false) + """ + Returns a new QuerySet instance that will select only distinct results. + """ + obj = self._clone() + obj.query.distinct = true_or_false + return obj - def extra(self, select=None, where=None, params=None, tables=None): - assert self._limit is None and self._offset is None, \ + def extra(self, select=None, where=None, params=None, tables=None, + order_by=None, select_params=None): + """ + Add extra SQL fragments to the query. + """ + assert self.query.can_filter(), \ "Cannot change a query once a slice has been taken" clone = self._clone() - if select: clone._select.update(select) - if where: clone._where.extend(where) - if params: clone._params.extend(params) - if tables: clone._tables.extend(tables) + clone.query.add_extra(select, select_params, where, params, tables, order_by) + return clone + + def reverse(self): + """ + Reverses the ordering of the queryset. + """ + clone = self._clone() + clone.query.standard_ordering = not clone.query.standard_ordering return clone ################### # PRIVATE METHODS # ################### - def _clone(self, klass=None, **kwargs): + def _clone(self, klass=None, setup=False, **kwargs): if klass is None: klass = self.__class__ - c = klass() - c.model = self.model - c._filters = self._filters - c._order_by = self._order_by - c._select_related = self._select_related - c._max_related_depth = self._max_related_depth - c._distinct = self._distinct - c._select = self._select.copy() - c._where = self._where[:] - c._params = self._params[:] - c._tables = self._tables[:] - c._offset = self._offset - c._limit = self._limit + c = klass(model=self.model, query=self.query.clone()) c.__dict__.update(kwargs) + if setup and hasattr(c, '_setup_query'): + c._setup_query() return c - def _combine(self, other): - assert self._limit is None and self._offset is None \ - and other._limit is None and other._offset is None, \ - "Cannot combine queries once a slice has been taken." - assert self._distinct == other._distinct, \ - "Cannot combine a unique query with a non-unique query" - # use 'other's order by - # (so that A.filter(args1) & A.filter(args2) does the same as - # A.filter(args1).filter(args2) - combined = other._clone() - if self._select: combined._select.update(self._select) - if self._where: combined._where.extend(self._where) - if self._params: combined._params.extend(self._params) - if self._tables: combined._tables.extend(self._tables) - # If 'self' is ordered and 'other' isn't, propagate 'self's ordering - if (self._order_by is not None and len(self._order_by) > 0) and \ - (combined._order_by is None or len(combined._order_by) == 0): - combined._order_by = self._order_by - return combined + def _fill_cache(self, num=None): + """ + Fills the result cache with 'num' more entries (or until the results + iterator is exhausted). + """ + if self._iter: + try: + for i in range(num or ITER_CHUNK_SIZE): + self._result_cache.append(self._iter.next()) + except StopIteration: + self._iter = None - def _get_data(self): - if self._result_cache is None: - self._result_cache = list(self.iterator()) - return self._result_cache - - def _get_sql_clause(self): - qn = connection.ops.quote_name - opts = self.model._meta - - # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. - select = ["%s.%s" % (qn(opts.db_table), qn(f.column)) for f in opts.fields] - tables = [quote_only_if_word(t) for t in self._tables] - joins = SortedDict() - where = self._where[:] - params = self._params[:] - - # Convert self._filters into SQL. - joins2, where2, params2 = self._filters.get_sql(opts) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - - # Add additional tables and WHERE clauses based on select_related. - if self._select_related: - fill_table_cache(opts, select, tables, where, - old_prefix=opts.db_table, - cache_tables_seen=[opts.db_table], - max_depth=self._max_related_depth) - - # Add any additional SELECTs. - if self._select: - select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), qn(s[0])) for s in self._select.items()]) - - # Start composing the body of the SQL statement. - sql = [" FROM", qn(opts.db_table)] - - # Compose the join dictionary into SQL describing the joins. - if joins: - sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition) - for (alias, (table, join_type, condition)) in joins.items()])) - - # Compose the tables clause into SQL. - if tables: - sql.append(", " + ", ".join(tables)) - - # Compose the where clause into SQL. - if where: - sql.append(where and "WHERE " + " AND ".join(where)) - - # ORDER BY clause - order_by = [] - if self._order_by is not None: - ordering_to_use = self._order_by - else: - ordering_to_use = opts.ordering - for f in handle_legacy_orderlist(ordering_to_use): - if f == '?': # Special case. - order_by.append(connection.ops.random_function_sql()) - else: - if f.startswith('-'): - col_name = f[1:] - order = "DESC" - else: - col_name = f - order = "ASC" - if "." in col_name: - table_prefix, col_name = col_name.split('.', 1) - table_prefix = qn(table_prefix) + '.' - else: - # Use the database table as a column prefix if it wasn't given, - # and if the requested column isn't a custom SELECT. - if "." not in col_name and col_name not in (self._select or ()): - table_prefix = qn(opts.db_table) + '.' - else: - table_prefix = '' - order_by.append('%s%s %s' % (table_prefix, qn(orderfield2column(col_name, opts)), order)) - if order_by: - sql.append("ORDER BY " + ", ".join(order_by)) - - # LIMIT and OFFSET clauses - if self._limit is not None: - sql.append("%s " % connection.ops.limit_offset_sql(self._limit, self._offset)) - else: - assert self._offset is None, "'offset' is not allowed without 'limit'" - - return select, " ".join(sql), params - -# Use the backend's QuerySet class if it defines one. Otherwise, use _QuerySet. -if connection.features.uses_custom_queryset: - QuerySet = connection.ops.query_set_class(_QuerySet) -else: - QuerySet = _QuerySet + def _merge_sanity_check(self, other): + """ + Checks that we are merging two comparable queryset classes. + """ + if self.__class__ is not other.__class__: + raise TypeError("Cannot merge querysets of different types ('%s' and '%s'." + % (self.__class__.__name__, other.__class__.__name__)) class ValuesQuerySet(QuerySet): def __init__(self, *args, **kwargs): super(ValuesQuerySet, self).__init__(*args, **kwargs) - # select_related isn't supported in values(). - self._select_related = False + # select_related isn't supported in values(). (FIXME -#3358) + self.query.select_related = False + + # QuerySet.clone() will also set up the _fields attribute with the + # names of the model fields to select. + + def __iter__(self): + return self.iterator() def iterator(self): - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - raise StopIteration + self.query.trim_extra_select(self.extra_names) + names = self.query.extra_select.keys() + self.field_names + for row in self.query.results_iter(): + yield dict(zip(names, row)) - qn = connection.ops.quote_name + def _setup_query(self): + """ + Constructs the field_names list that the values query will be + retrieving. - # self._select is a dictionary, and dictionaries' key order is - # undefined, so we convert it to a list of tuples. - extra_select = self._select.items() - - # Construct two objects -- fields and field_names. - # fields is a list of Field objects to fetch. - # field_names is a list of field names, which will be the keys in the - # resulting dictionaries. + Called by the _clone() method after initialising the rest of the + instance. + """ + self.extra_names = [] if self._fields: - if not extra_select: - fields = [self.model._meta.get_field(f, many_to_many=False) for f in self._fields] - field_names = self._fields + if not self.query.extra_select: + field_names = list(self._fields) else: - fields = [] field_names = [] for f in self._fields: - if f in [field.name for field in self.model._meta.fields]: - fields.append(self.model._meta.get_field(f, many_to_many=False)) + if self.query.extra_select.has_key(f): + self.extra_names.append(f) + else: field_names.append(f) - elif not self._select.has_key(f): - raise FieldDoesNotExist('%s has no field named %r' % (self.model._meta.object_name, f)) - else: # Default to all fields. - fields = self.model._meta.fields - field_names = [f.attname for f in fields] + else: + # Default to all fields. + field_names = [f.attname for f in self.model._meta.fields] - columns = [f.column for f in fields] - select = ['%s.%s' % (qn(self.model._meta.db_table), qn(c)) for c in columns] - if extra_select: - select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), qn(s[0])) for s in extra_select]) - field_names.extend([f[0] for f in extra_select]) + self.query.add_fields(field_names, False) + self.query.default_cols = False + self.field_names = field_names - cursor = connection.cursor() - cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) - - has_resolve_columns = hasattr(self, 'resolve_columns') - while 1: - rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) - if not rows: - raise StopIteration - for row in rows: - if has_resolve_columns: - row = self.resolve_columns(row, fields) - yield dict(zip(field_names, row)) - - def _clone(self, klass=None, **kwargs): + def _clone(self, klass=None, setup=False, **kwargs): + """ + Cloning a ValuesQuerySet preserves the current fields. + """ c = super(ValuesQuerySet, self)._clone(klass, **kwargs) c._fields = self._fields[:] + c.field_names = self.field_names + c.extra_names = self.extra_names + if setup and hasattr(c, '_setup_query'): + c._setup_query() return c + def _merge_sanity_check(self, other): + super(ValuesQuerySet, self)._merge_sanity_check(other) + if (set(self.extra_names) != set(other.extra_names) or + set(self.field_names) != set(other.field_names)): + raise TypeError("Merging '%s' classes must involve the same values in each case." + % self.__class__.__name__) + +class ValuesListQuerySet(ValuesQuerySet): + def iterator(self): + self.query.trim_extra_select(self.extra_names) + if self.flat and len(self._fields) == 1: + for row in self.query.results_iter(): + yield row[0] + elif not self.query.extra_select: + for row in self.query.results_iter(): + yield row + else: + # When extra(select=...) is involved, the extra cols come are + # always at the start of the row, so we need to reorder the fields + # to match the order in self._fields. + names = self.query.extra_select.keys() + self.field_names + for row in self.query.results_iter(): + data = dict(zip(names, row)) + yield tuple([data[f] for f in self._fields]) + + def _clone(self, *args, **kwargs): + clone = super(ValuesListQuerySet, self)._clone(*args, **kwargs) + clone.flat = self.flat + return clone + class DateQuerySet(QuerySet): def iterator(self): - from django.db.backends.util import typecast_timestamp - from django.db.models.fields import DateTimeField + return self.query.results_iter() - qn = connection.ops.quote_name - self._order_by = () # Clear this because it'll mess things up otherwise. + def _setup_query(self): + """ + Sets up any special features of the query attribute. + + Called by the _clone() method after initialising the rest of the + instance. + """ + self.query = self.query.clone(klass=sql.DateQuery, setup=True) + self.query.select = [] + self.query.add_date_select(self._field.column, self._kind, self._order) if self._field.null: - self._where.append('%s.%s IS NOT NULL' % \ - (qn(self.model._meta.db_table), qn(self._field.column))) - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - raise StopIteration + self.query.add_filter(('%s__isnull' % self._field.name, True)) - table_name = qn(self.model._meta.db_table) - field_name = qn(self._field.column) - - if connection.features.allows_group_by_ordinal: - group_by = '1' - else: - group_by = connection.ops.date_trunc_sql(self._kind, '%s.%s' % (table_name, field_name)) - - sql = 'SELECT %s %s GROUP BY %s ORDER BY 1 %s' % \ - (connection.ops.date_trunc_sql(self._kind, '%s.%s' % (qn(self.model._meta.db_table), - qn(self._field.column))), sql, group_by, self._order) - cursor = connection.cursor() - cursor.execute(sql, params) - - has_resolve_columns = hasattr(self, 'resolve_columns') - needs_datetime_string_cast = connection.features.needs_datetime_string_cast - dates = [] - # It would be better to use self._field here instead of DateTimeField(), - # but in Oracle that will result in a list of datetime.date instead of - # datetime.datetime. - fields = [DateTimeField()] - while 1: - rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) - if not rows: - return dates - for row in rows: - date = row[0] - if has_resolve_columns: - date = self.resolve_columns([date], fields)[0] - elif needs_datetime_string_cast: - date = typecast_timestamp(str(date)) - dates.append(date) - - def _clone(self, klass=None, **kwargs): - c = super(DateQuerySet, self)._clone(klass, **kwargs) + def _clone(self, klass=None, setup=False, **kwargs): + c = super(DateQuerySet, self)._clone(klass, False, **kwargs) c._field = self._field c._kind = self._kind - c._order = self._order + if setup and hasattr(c, '_setup_query'): + c._setup_query() return c class EmptyQuerySet(QuerySet): - def __init__(self, model=None): - super(EmptyQuerySet, self).__init__(model) + def __init__(self, model=None, query=None): + super(EmptyQuerySet, self).__init__(model, query) self._result_cache = [] def count(self): @@ -703,488 +610,112 @@ class EmptyQuerySet(QuerySet): def delete(self): pass - def _clone(self, klass=None, **kwargs): + def _clone(self, klass=None, setup=False, **kwargs): c = super(EmptyQuerySet, self)._clone(klass, **kwargs) c._result_cache = [] return c - def _get_sql_clause(self): - raise EmptyResultSet + def iterator(self): + # This slightly odd construction is because we need an empty generator + # (it raises StopIteration immediately). + yield iter([]).next() -class QOperator(object): - "Base class for QAnd and QOr" - def __init__(self, *args): - self.args = args +# QOperator, QNot, QAnd and QOr are temporarily retained for backwards +# compatibility. All the old functionality is now part of the 'Q' class. +class QOperator(Q): + def __init__(self, *args, **kwargs): + warnings.warn('Use Q instead of QOr, QAnd or QOperation.', + DeprecationWarning, stacklevel=2) + super(QOperator, self).__init__(*args, **kwargs) - def get_sql(self, opts): - joins, where, params = SortedDict(), [], [] - for val in self.args: - try: - joins2, where2, params2 = val.get_sql(opts) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - except EmptyResultSet: - if not isinstance(self, QOr): - raise EmptyResultSet - if where: - return joins, ['(%s)' % self.operator.join(where)], params - return joins, [], params +QOr = QAnd = QOperator -class QAnd(QOperator): - "Encapsulates a combined query that uses 'AND'." - operator = ' AND ' - def __or__(self, other): - return QOr(self, other) +def QNot(q): + warnings.warn('Use ~q instead of QNot(q)', DeprecationWarning, stacklevel=2) + return ~q - def __and__(self, other): - if isinstance(other, QAnd): - return QAnd(*(self.args+other.args)) - elif isinstance(other, (Q, QOr)): - return QAnd(*(self.args+(other,))) - else: - raise TypeError, other - -class QOr(QOperator): - "Encapsulates a combined query that uses 'OR'." - operator = ' OR ' - def __and__(self, other): - return QAnd(self, other) - - def __or__(self, other): - if isinstance(other, QOr): - return QOr(*(self.args+other.args)) - elif isinstance(other, (Q, QAnd)): - return QOr(*(self.args+(other,))) - else: - raise TypeError, other - -class Q(object): - "Encapsulates queries as objects that can be combined logically." - def __init__(self, **kwargs): - self.kwargs = kwargs - - def __and__(self, other): - return QAnd(self, other) - - def __or__(self, other): - return QOr(self, other) - - def get_sql(self, opts): - return parse_lookup(self.kwargs.items(), opts) - -class QNot(Q): - "Encapsulates NOT (...) queries as objects" - def __init__(self, q): - "Creates a negation of the q object passed in." - self.q = q - - def get_sql(self, opts): - try: - joins, where, params = self.q.get_sql(opts) - where2 = ['(NOT (%s))' % " AND ".join(where)] - except EmptyResultSet: - return SortedDict(), [], [] - return joins, where2, params - -def get_where_clause(lookup_type, table_prefix, field_name, value, db_type): - if table_prefix.endswith('.'): - table_prefix = connection.ops.quote_name(table_prefix[:-1])+'.' - field_name = connection.ops.quote_name(field_name) - if type(value) == datetime.datetime and connection.ops.datetime_cast_sql(): - cast_sql = connection.ops.datetime_cast_sql() - else: - cast_sql = '%s' - field_sql = connection.ops.field_cast_sql(db_type) % (table_prefix + field_name) - if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith') and connection.features.needs_upper_for_iops: - format = 'UPPER(%s) %s' - else: - format = '%s %s' - try: - return format % (field_sql, connection.operators[lookup_type] % cast_sql) - except KeyError: - pass - if lookup_type == 'in': - in_string = ','.join(['%s' for id in value]) - if in_string: - return '%s IN (%s)' % (field_sql, in_string) - else: - raise EmptyResultSet - elif lookup_type in ('range', 'year'): - return '%s BETWEEN %%s AND %%s' % field_sql - elif lookup_type in ('month', 'day'): - return "%s = %%s" % connection.ops.date_extract_sql(lookup_type, field_sql) - elif lookup_type == 'isnull': - return "%s IS %sNULL" % (field_sql, (not value and 'NOT ' or '')) - elif lookup_type == 'search': - return connection.ops.fulltext_search_sql(field_sql) - elif lookup_type in ('regex', 'iregex'): - if settings.DATABASE_ENGINE == 'oracle': - if connection.oracle_version and connection.oracle_version <= 9: - msg = "Regexes are not supported in Oracle before version 10g." - raise NotImplementedError(msg) - if lookup_type == 'regex': - match_option = 'c' - else: - match_option = 'i' - return "REGEXP_LIKE(%s, %s, '%s')" % (field_sql, cast_sql, match_option) - else: - raise NotImplementedError - raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) - -def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0): - """Helper function that recursively returns an object with cache filled""" - - # If we've got a max_depth set and we've exceeded that depth, bail now. - if max_depth and cur_depth > max_depth: +def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0, + requested=None): + """ + Helper function that recursively returns an object with the specified + related attributes already populated. + """ + if max_depth and requested is None and cur_depth > max_depth: + # We've recursed deeply enough; stop now. return None + restricted = requested is not None index_end = index_start + len(klass._meta.fields) - obj = klass(*row[index_start:index_end]) + obj = klass.from_sequence(row[index_start:index_end]) for f in klass._meta.fields: - if f.rel and not f.null: - cached_row = get_cached_row(f.rel.to, row, index_end, max_depth, cur_depth+1) - if cached_row: - rel_obj, index_end = cached_row - setattr(obj, f.get_cache_name(), rel_obj) + if (not f.rel or (not restricted and f.null) or + (restricted and f.name not in requested) or f.rel.parent_link): + continue + if restricted: + next = requested[f.name] + else: + next = None + cached_row = get_cached_row(f.rel.to, row, index_end, max_depth, + cur_depth+1, next) + if cached_row: + rel_obj, index_end = cached_row + setattr(obj, f.get_cache_name(), rel_obj) return obj, index_end -def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen, max_depth=0, cur_depth=0): - """ - Helper function that recursively populates the select, tables and where (in - place) for select_related queries. - """ - - # If we've got a max_depth set and we've exceeded that depth, bail now. - if max_depth and cur_depth > max_depth: - return None - - qn = connection.ops.quote_name - for f in opts.fields: - if f.rel and not f.null: - db_table = f.rel.to._meta.db_table - if db_table not in cache_tables_seen: - tables.append(qn(db_table)) - else: # The table was already seen, so give it a table alias. - new_prefix = '%s%s' % (db_table, len(cache_tables_seen)) - tables.append('%s %s' % (qn(db_table), qn(new_prefix))) - db_table = new_prefix - cache_tables_seen.append(db_table) - where.append('%s.%s = %s.%s' % \ - (qn(old_prefix), qn(f.column), qn(db_table), qn(f.rel.get_related_field().column))) - select.extend(['%s.%s' % (qn(db_table), qn(f2.column)) for f2 in f.rel.to._meta.fields]) - fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen, max_depth, cur_depth+1) - -def parse_lookup(kwarg_items, opts): - # Helper function that handles converting API kwargs - # (e.g. "name__exact": "tom") to SQL. - # Returns a tuple of (joins, where, params). - - # 'joins' is a sorted dictionary describing the tables that must be joined - # to complete the query. The dictionary is sorted because creation order - # is significant; it is a dictionary to ensure uniqueness of alias names. - # - # Each key-value pair follows the form - # alias: (table, join_type, condition) - # where - # alias is the AS alias for the joined table - # table is the actual table name to be joined - # join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc) - # condition is the where-like statement over which narrows the join. - # alias will be derived from the lookup list name. - # - # At present, this method only every returns INNER JOINs; the option is - # there for others to implement custom Q()s, etc that return other join - # types. - joins, where, params = SortedDict(), [], [] - - for kwarg, value in kwarg_items: - path = kwarg.split(LOOKUP_SEPARATOR) - # Extract the last elements of the kwarg. - # The very-last is the lookup_type (equals, like, etc). - # The second-last is the table column on which the lookup_type is - # to be performed. If this name is 'pk', it will be substituted with - # the name of the primary key. - # If there is only one part, or the last part is not a query - # term, assume that the query is an __exact - lookup_type = path.pop() - if lookup_type == 'pk': - lookup_type = 'exact' - path.append(None) - elif len(path) == 0 or lookup_type not in QUERY_TERMS: - path.append(lookup_type) - lookup_type = 'exact' - - if len(path) < 1: - raise TypeError, "Cannot parse keyword query %r" % kwarg - - if value is None: - # Interpret '__exact=None' as the sql '= NULL'; otherwise, reject - # all uses of None as a query value. - if lookup_type != 'exact': - raise ValueError, "Cannot use None as a query value" - elif callable(value): - value = value() - - joins2, where2, params2 = lookup_inner(path, lookup_type, value, opts, opts.db_table, None) - joins.update(joins2) - where.extend(where2) - params.extend(params2) - return joins, where, params - -class FieldFound(Exception): - "Exception used to short circuit field-finding operations." - pass - -def find_field(name, field_list, related_query): - """ - Finds a field with a specific name in a list of field instances. - Returns None if there are no matches, or several matches. - """ - if related_query: - matches = [f for f in field_list if f.field.related_query_name() == name] - else: - matches = [f for f in field_list if f.name == name] - if len(matches) != 1: - return None - return matches[0] - -def field_choices(field_list, related_query): - if related_query: - choices = [f.field.related_query_name() for f in field_list] - else: - choices = [f.name for f in field_list] - return choices - -def lookup_inner(path, lookup_type, value, opts, table, column): - qn = connection.ops.quote_name - joins, where, params = SortedDict(), [], [] - current_opts = opts - current_table = table - current_column = column - intermediate_table = None - join_required = False - - name = path.pop(0) - # Has the primary key been requested? If so, expand it out - # to be the name of the current class' primary key - if name is None or name == 'pk': - name = current_opts.pk.name - - # Try to find the name in the fields associated with the current class - try: - # Does the name belong to a defined many-to-many field? - field = find_field(name, current_opts.many_to_many, False) - if field: - new_table = current_table + '__' + name - new_opts = field.rel.to._meta - new_column = new_opts.pk.column - - # Need to create an intermediate table join over the m2m table - # This process hijacks current_table/column to point to the - # intermediate table. - current_table = "m2m_" + new_table - intermediate_table = field.m2m_db_table() - join_column = field.m2m_reverse_name() - intermediate_column = field.m2m_column_name() - - raise FieldFound - - # Does the name belong to a reverse defined many-to-many field? - field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True) - if field: - new_table = current_table + '__' + name - new_opts = field.opts - new_column = new_opts.pk.column - - # Need to create an intermediate table join over the m2m table. - # This process hijacks current_table/column to point to the - # intermediate table. - current_table = "m2m_" + new_table - intermediate_table = field.field.m2m_db_table() - join_column = field.field.m2m_column_name() - intermediate_column = field.field.m2m_reverse_name() - - raise FieldFound - - # Does the name belong to a one-to-many field? - field = find_field(name, current_opts.get_all_related_objects(), True) - if field: - new_table = table + '__' + name - new_opts = field.opts - new_column = field.field.column - join_column = opts.pk.column - - # 1-N fields MUST be joined, regardless of any other conditions. - join_required = True - - raise FieldFound - - # Does the name belong to a one-to-one, many-to-one, or regular field? - field = find_field(name, current_opts.fields, False) - if field: - if field.rel: # One-to-One/Many-to-one field - new_table = current_table + '__' + name - new_opts = field.rel.to._meta - new_column = new_opts.pk.column - join_column = field.column - raise FieldFound - elif path: - # For regular fields, if there are still items on the path, - # an error has been made. We munge "name" so that the error - # properly identifies the cause of the problem. - name += LOOKUP_SEPARATOR + path[0] - else: - raise FieldFound - - except FieldFound: # Match found, loop has been shortcut. - pass - else: # No match found. - choices = field_choices(current_opts.many_to_many, False) + \ - field_choices(current_opts.get_all_related_many_to_many_objects(), True) + \ - field_choices(current_opts.get_all_related_objects(), True) + \ - field_choices(current_opts.fields, False) - raise TypeError, "Cannot resolve keyword '%s' into field. Choices are: %s" % (name, ", ".join(choices)) - - # Check whether an intermediate join is required between current_table - # and new_table. - if intermediate_table: - joins[qn(current_table)] = ( - qn(intermediate_table), "LEFT OUTER JOIN", - "%s.%s = %s.%s" % (qn(table), qn(current_opts.pk.column), qn(current_table), qn(intermediate_column)) - ) - - if path: - # There are elements left in the path. More joins are required. - if len(path) == 1 and path[0] in (new_opts.pk.name, None) \ - and lookup_type in ('exact', 'isnull') and not join_required: - # If the next and final name query is for a primary key, - # and the search is for isnull/exact, then the current - # (for N-1) or intermediate (for N-N) table can be used - # for the search. No need to join an extra table just - # to check the primary key. - new_table = current_table - else: - # There are 1 or more name queries pending, and we have ruled out - # any shortcuts; therefore, a join is required. - joins[qn(new_table)] = ( - qn(new_opts.db_table), "INNER JOIN", - "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) - ) - # If we have made the join, we don't need to tell subsequent - # recursive calls about the column name we joined on. - join_column = None - - # There are name queries remaining. Recurse deeper. - joins2, where2, params2 = lookup_inner(path, lookup_type, value, new_opts, new_table, join_column) - - joins.update(joins2) - where.extend(where2) - params.extend(params2) - else: - # No elements left in path. Current element is the element on which - # the search is being performed. - db_type = None - - if join_required: - # Last query term is a RelatedObject - if field.field.rel.multiple: - # RelatedObject is from a 1-N relation. - # Join is required; query operates on joined table. - column = new_opts.pk.name - joins[qn(new_table)] = ( - qn(new_opts.db_table), "INNER JOIN", - "%s.%s = %s.%s" % (qn(current_table), qn(join_column), qn(new_table), qn(new_column)) - ) - current_table = new_table - else: - # RelatedObject is from a 1-1 relation, - # No need to join; get the pk value from the related object, - # and compare using that. - column = current_opts.pk.name - elif intermediate_table: - # Last query term is a related object from an N-N relation. - # Join from intermediate table is sufficient. - column = join_column - elif name == current_opts.pk.name and lookup_type in ('exact', 'isnull') and current_column: - # Last query term is for a primary key. If previous iterations - # introduced a current/intermediate table that can be used to - # optimize the query, then use that table and column name. - column = current_column - else: - # Last query term was a normal field. - column = field.column - db_type = field.db_type() - - where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type)) - params.extend(field.get_db_prep_lookup(lookup_type, value)) - - return joins, where, params - def delete_objects(seen_objs): - "Iterate through a list of seen classes, and remove any instances that are referred to" - qn = connection.ops.quote_name + """ + Iterate through a list of seen classes, and remove any instances that are + referred to. + """ ordered_classes = seen_objs.keys() ordered_classes.reverse() - cursor = connection.cursor() - for cls in ordered_classes: seen_objs[cls] = seen_objs[cls].items() seen_objs[cls].sort() # Pre notify all instances to be deleted for pk_val, instance in seen_objs[cls]: - dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance) + dispatcher.send(signal=signals.pre_delete, sender=cls, + instance=instance) pk_list = [pk for pk,instance in seen_objs[cls]] - for related in cls._meta.get_all_related_many_to_many_objects(): - if not isinstance(related.field, generic.GenericRelation): - for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ - (qn(related.field.m2m_db_table()), - qn(related.field.m2m_reverse_name()), - ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), - pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) - for f in cls._meta.many_to_many: - if isinstance(f, generic.GenericRelation): - from django.contrib.contenttypes.models import ContentType - query_extra = 'AND %s=%%s' % f.rel.to._meta.get_field(f.content_type_field_name).column - args_extra = [ContentType.objects.get_for_model(cls).id] - else: - query_extra = '' - args_extra = [] - for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - cursor.execute(("DELETE FROM %s WHERE %s IN (%s)" % \ - (qn(f.m2m_db_table()), qn(f.m2m_column_name()), - ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]]))) + query_extra, - pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE] + args_extra) + del_query = sql.DeleteQuery(cls, connection) + del_query.delete_batch_related(pk_list) + + update_query = sql.UpdateQuery(cls, connection) for field in cls._meta.fields: if field.rel and field.null and field.rel.to in seen_objs: - for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - cursor.execute("UPDATE %s SET %s=NULL WHERE %s IN (%s)" % \ - (qn(cls._meta.db_table), qn(field.column), qn(cls._meta.pk.column), - ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), - pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + update_query.clear_related(field, pk_list) # Now delete the actual data for cls in ordered_classes: seen_objs[cls].reverse() pk_list = [pk for pk,instance in seen_objs[cls]] - for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): - cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ - (qn(cls._meta.db_table), qn(cls._meta.pk.column), - ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), - pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + del_query = sql.DeleteQuery(cls, connection) + del_query.delete_batch(pk_list) - # Last cleanup; set NULLs where there once was a reference to the object, - # NULL the primary key of the found objects, and perform post-notification. + # Last cleanup; set NULLs where there once was a reference to the + # object, NULL the primary key of the found objects, and perform + # post-notification. for pk_val, instance in seen_objs[cls]: for field in cls._meta.fields: if field.rel and field.null and field.rel.to in seen_objs: setattr(instance, field.attname, None) - dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) + dispatcher.send(signal=signals.post_delete, sender=cls, + instance=instance) setattr(instance, cls._meta.pk.attname, None) transaction.commit_unless_managed() + +def insert_query(model, values, return_id=False, raw_values=False): + """ + Inserts a new record for the given model. This provides an interface to + the InsertQuery class and is how Model.save() is implemented. It is not + part of the public API. + """ + query = sql.InsertQuery(model, connection) + query.insert_values(values, raw_values) + return query.execute_sql(return_id) + diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py new file mode 100644 index 0000000000..0ce7900c74 --- /dev/null +++ b/django/db/models/query_utils.py @@ -0,0 +1,50 @@ +""" +Various data structures used in query construction. + +Factored out from django.db.models.query so that they can also be used by other +modules without getting into circular import difficulties. +""" + +from copy import deepcopy + +from django.utils import tree + +class QueryWrapper(object): + """ + A type that indicates the contents are an SQL fragment and the associate + parameters. Can be used to pass opaque data to a where-clause, for example. + """ + def __init__(self, sql, params): + self.data = sql, params + +class Q(tree.Node): + """ + Encapsulates filters as objects that can then be combined logically (using + & and |). + """ + # Connection types + AND = 'AND' + OR = 'OR' + default = AND + + def __init__(self, *args, **kwargs): + super(Q, self).__init__(children=list(args) + kwargs.items()) + + def _combine(self, other, conn): + if not isinstance(other, Q): + raise TypeError(other) + obj = deepcopy(self) + obj.add(other, conn) + return obj + + def __or__(self, other): + return self._combine(other, self.OR) + + def __and__(self, other): + return self._combine(other, self.AND) + + def __invert__(self): + obj = deepcopy(self) + obj.negate() + return obj + diff --git a/django/db/models/sql/__init__.py b/django/db/models/sql/__init__.py new file mode 100644 index 0000000000..7310982690 --- /dev/null +++ b/django/db/models/sql/__init__.py @@ -0,0 +1,7 @@ +from query import * +from subqueries import * +from where import AND, OR +from datastructures import EmptyResultSet + +__all__ = ['Query', 'AND', 'OR', 'EmptyResultSet'] + diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py new file mode 100644 index 0000000000..3075817385 --- /dev/null +++ b/django/db/models/sql/constants.py @@ -0,0 +1,36 @@ +import re + +# Valid query types (a dictionary is used for speedy lookups). +QUERY_TERMS = dict([(x, None) for x in ( + 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', + 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', + 'month', 'day', 'isnull', 'search', 'regex', 'iregex', + )]) + +# Size of each "chunk" for get_iterator calls. +# Larger values are slightly faster at the expense of more storage space. +GET_ITERATOR_CHUNK_SIZE = 100 + +# Separator used to split filter strings apart. +LOOKUP_SEP = '__' + +# Constants to make looking up tuple values clearer. +# Join lists +TABLE_NAME = 0 +RHS_ALIAS = 1 +JOIN_TYPE = 2 +LHS_ALIAS = 3 +LHS_JOIN_COL = 4 +RHS_JOIN_COL = 5 +NULLABLE = 6 + +# How many results to expect from a cursor.execute call +MULTI = 'multi' +SINGLE = 'single' + +ORDER_PATTERN = re.compile(r'\?|[-+]?\w+$') +ORDER_DIR = { + 'ASC': ('ASC', 'DESC'), + 'DESC': ('DESC', 'ASC')} + + diff --git a/django/db/models/sql/datastructures.py b/django/db/models/sql/datastructures.py new file mode 100644 index 0000000000..cb54a564f8 --- /dev/null +++ b/django/db/models/sql/datastructures.py @@ -0,0 +1,103 @@ +""" +Useful auxilliary data structures for query construction. Not useful outside +the SQL domain. +""" + +class EmptyResultSet(Exception): + pass + +class FullResultSet(Exception): + pass + +class MultiJoin(Exception): + """ + Used by join construction code to indicate the point at which a + multi-valued join was attempted (if the caller wants to treat that + exceptionally). + """ + def __init__(self, level): + self.level = level + +class Empty(object): + pass + +class RawValue(object): + def __init__(self, value): + self.value = value + +class Aggregate(object): + """ + Base class for all aggregate-related classes (min, max, avg, count, sum). + """ + def relabel_aliases(self, change_map): + """ + Relabel the column alias, if necessary. Must be implemented by + subclasses. + """ + raise NotImplementedError + + def as_sql(self, quote_func=None): + """ + Returns the SQL string fragment for this object. + + The quote_func function is used to quote the column components. If + None, it defaults to doing nothing. + + Must be implemented by subclasses. + """ + raise NotImplementedError + +class Count(Aggregate): + """ + Perform a count on the given column. + """ + def __init__(self, col='*', distinct=False): + """ + Set the column to count on (defaults to '*') and set whether the count + should be distinct or not. + """ + self.col = col + self.distinct = distinct + + def relabel_aliases(self, change_map): + c = self.col + if isinstance(c, (list, tuple)): + self.col = (change_map.get(c[0], c[0]), c[1]) + + def as_sql(self, quote_func=None): + if not quote_func: + quote_func = lambda x: x + if isinstance(self.col, (list, tuple)): + col = ('%s.%s' % tuple([quote_func(c) for c in self.col])) + elif hasattr(self.col, 'as_sql'): + col = self.col.as_sql(quote_func) + else: + col = self.col + if self.distinct: + return 'COUNT(DISTINCT %s)' % col + else: + return 'COUNT(%s)' % col + +class Date(object): + """ + Add a date selection column. + """ + def __init__(self, col, lookup_type, date_sql_func): + self.col = col + self.lookup_type = lookup_type + self.date_sql_func= date_sql_func + + def relabel_aliases(self, change_map): + c = self.col + if isinstance(c, (list, tuple)): + self.col = (change_map.get(c[0], c[0]), c[1]) + + def as_sql(self, quote_func=None): + if not quote_func: + quote_func = lambda x: x + if isinstance(self.col, (list, tuple)): + col = '%s.%s' % tuple([quote_func(c) for c in self.col]) + else: + col = self.col + return self.date_sql_func(self.lookup_type, col) + diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py new file mode 100644 index 0000000000..59b2ebdd68 --- /dev/null +++ b/django/db/models/sql/query.py @@ -0,0 +1,1504 @@ +""" +Create SQL statements for QuerySets. + +The code in here encapsulates all of the SQL construction so that QuerySets +themselves do not have to (and could be backed by things other than SQL +databases). The abstraction barrier only works one way: this module has to know +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.dispatch import dispatcher +from django.db import connection +from django.db.models import signals +from django.db.models.sql.where import WhereNode, EverythingNode, AND, OR +from django.db.models.sql.datastructures import Count +from django.db.models.fields import FieldDoesNotExist +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): + """ + A single SQL query. + """ + # SQL join types. These are part of the class because their string forms + # vary from database to database and can be customised by a subclass. + INNER = 'INNER JOIN' + LOUTER = 'LEFT OUTER JOIN' + + alias_prefix = 'T' + query_terms = QUERY_TERMS + + def __init__(self, model, connection, where=WhereNode): + self.model = model + self.connection = connection + self.alias_refcount = {} + self.alias_map = {} # Maps alias to join information + self.table_map = {} # Maps table names to list of aliases. + self.join_map = {} + self.rev_join_map = {} # Reverse of join_map. + self.quote_cache = {} + self.default_cols = True + self.default_ordering = True + self.standard_ordering = True + self.ordering_aliases = [] + self.start_meta = None + self.select_fields = [] + self.related_select_fields = [] + + # SQL-related attributes + self.select = [] + self.tables = [] # Aliases in the order they are created. + self.where = where() + self.where_class = where + self.group_by = [] + self.having = [] + self.order_by = [] + self.low_mark, self.high_mark = 0, None # Used for offset/limit + self.distinct = False + self.select_related = False + self.related_select_cols = [] + + # Arbitrary maximum limit for select_related. Prevents infinite + # recursion. Can be changed by the depth parameter to select_related(). + self.max_depth = 5 + + # These are for extensions. The contents are more or less appended + # verbatim to the appropriate clause. + self.extra_select = {} # Maps col_alias -> col_sql. + self.extra_select_params = () + self.extra_tables = () + self.extra_where = () + self.extra_params = () + self.extra_order_by = () + + def __str__(self): + """ + Returns the query as a string of SQL with the parameter values + substituted in. + + Parameter values won't necessarily be quoted correctly, since that is + done by the database interface at execution time. + """ + sql, params = self.as_sql() + return sql % params + + def __deepcopy__(self, memo): + result= self.clone() + memo[id(self)] = result + return result + + def get_meta(self): + """ + Returns the Options instance (the model._meta) from which to start + processing. Normally, this is self.model._meta, but it can change. + """ + if self.start_meta: + return self.start_meta + return self.model._meta + + def quote_name_unless_alias(self, name): + """ + A wrapper around connection.ops.quote_name that doesn't quote aliases + for table names. This avoids problems with some SQL dialects that treat + quoted strings specially (e.g. PostgreSQL). + """ + if name in self.quote_cache: + return self.quote_cache[name] + if ((name in self.alias_map and name not in self.table_map) or + name in self.extra_select): + self.quote_cache[name] = name + return name + r = self.connection.ops.quote_name(name) + self.quote_cache[name] = r + return r + + def clone(self, klass=None, **kwargs): + """ + Creates a copy of the current instance. The 'kwargs' parameter can be + used by clients to update attributes after copying has taken place. + """ + obj = Empty() + obj.__class__ = klass or self.__class__ + obj.model = self.model + obj.connection = self.connection + obj.alias_refcount = self.alias_refcount.copy() + obj.alias_map = self.alias_map.copy() + obj.table_map = self.table_map.copy() + obj.join_map = self.join_map.copy() + obj.rev_join_map = self.rev_join_map.copy() + obj.quote_cache = {} + obj.default_cols = self.default_cols + obj.default_ordering = self.default_ordering + obj.standard_ordering = self.standard_ordering + obj.ordering_aliases = [] + obj.start_meta = self.start_meta + obj.select_fields = self.select_fields[:] + obj.related_select_fields = self.related_select_fields[:] + obj.select = self.select[:] + obj.tables = self.tables[:] + obj.where = deepcopy(self.where) + obj.where_class = self.where_class + obj.group_by = self.group_by[:] + obj.having = self.having[:] + obj.order_by = self.order_by[:] + obj.low_mark, obj.high_mark = self.low_mark, self.high_mark + obj.distinct = self.distinct + obj.select_related = self.select_related + obj.related_select_cols = [] + obj.max_depth = self.max_depth + obj.extra_select = self.extra_select.copy() + obj.extra_select_params = self.extra_select_params + obj.extra_tables = self.extra_tables + obj.extra_where = self.extra_where + obj.extra_params = self.extra_params + obj.extra_order_by = self.extra_order_by + obj.__dict__.update(kwargs) + if hasattr(obj, '_setup_query'): + obj._setup_query() + return obj + + def results_iter(self): + """ + Returns an iterator over the results from executing this query. + """ + resolve_columns = hasattr(self, 'resolve_columns') + if resolve_columns: + if self.select_fields: + fields = self.select_fields + self.related_select_fields + else: + fields = self.model._meta.fields + for rows in self.execute_sql(MULTI): + for row in rows: + if resolve_columns: + row = self.resolve_columns(row, fields) + yield row + + def get_count(self): + """ + Performs a COUNT() query using the current filter constraints. + """ + from subqueries import CountQuery + obj = self.clone() + obj.clear_ordering(True) + obj.clear_limits() + obj.select_related = False + obj.related_select_cols = [] + obj.related_select_fields = [] + if obj.distinct and len(obj.select) > 1: + obj = self.clone(CountQuery, _query=obj, where=self.where_class(), + distinct=False) + obj.select = [] + obj.extra_select = {} + obj.add_count_column() + data = obj.execute_sql(SINGLE) + if not data: + return 0 + number = data[0] + + # Apply offset and limit constraints manually, since using LIMIT/OFFSET + # in SQL (in variants that provide them) doesn't change the COUNT + # output. + number = max(0, number - self.low_mark) + if self.high_mark: + number = min(number, self.high_mark - self.low_mark) + + return number + + def as_sql(self, with_limits=True, with_col_aliases=False): + """ + Creates the SQL for this query. Returns the SQL string and list of + parameters. + + If 'with_limits' is False, any limit/offset information is not included + in the query. + """ + self.pre_sql_setup() + out_cols = self.get_columns(with_col_aliases) + ordering = self.get_ordering() + + # This must come after 'select' and 'ordering' -- see docstring of + # get_from_clause() for details. + from_, f_params = self.get_from_clause() + + where, w_params = self.where.as_sql(qn=self.quote_name_unless_alias) + params = list(self.extra_select_params) + + result = ['SELECT'] + if self.distinct: + result.append('DISTINCT') + result.append(', '.join(out_cols + self.ordering_aliases)) + + result.append('FROM') + result.extend(from_) + params.extend(f_params) + + if where: + result.append('WHERE %s' % where) + params.extend(w_params) + if self.extra_where: + if not where: + result.append('WHERE') + else: + result.append('AND') + result.append(' AND '.join(self.extra_where)) + + if self.group_by: + grouping = self.get_grouping() + result.append('GROUP BY %s' % ', '.join(grouping)) + + if ordering: + result.append('ORDER BY %s' % ', '.join(ordering)) + + # FIXME: Pull this out to make life easier for Oracle et al. + if with_limits: + if self.high_mark: + result.append('LIMIT %d' % (self.high_mark - self.low_mark)) + if self.low_mark: + if not self.high_mark: + val = self.connection.ops.no_limit_value() + if val: + result.append('LIMIT %d' % val) + result.append('OFFSET %d' % self.low_mark) + + params.extend(self.extra_params) + return ' '.join(result), tuple(params) + + def combine(self, rhs, connector): + """ + Merge the 'rhs' query into the current one (with any 'rhs' effects + being applied *after* (that is, "to the right of") anything in the + current query. 'rhs' is not modified during a call to this function. + + The 'connector' parameter describes how to connect filters from the + 'rhs' query. + """ + assert self.model == rhs.model, \ + "Cannot combine queries on two different base models." + assert self.can_filter(), \ + "Cannot combine queries once a slice has been taken." + assert self.distinct == rhs.distinct, \ + "Cannot combine a unique query with a non-unique query." + + # Work out how to relabel the rhs aliases, if necessary. + change_map = {} + used = set() + conjunction = (connector == AND) + first = True + for alias in rhs.tables: + if not rhs.alias_refcount[alias]: + # An unused alias. + continue + promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER) + new_alias = self.join(rhs.rev_join_map[alias], + (conjunction and not first), used, promote, not conjunction) + used.add(new_alias) + change_map[alias] = new_alias + first = False + + # So that we don't exclude valid results in an "or" query combination, + # the first join that is exclusive to the lhs (self) must be converted + # to an outer join. + if not conjunction: + for alias in self.tables[1:]: + if self.alias_refcount[alias] == 1: + self.promote_alias(alias, True) + break + + # Now relabel a copy of the rhs where-clause and add it to the current + # one. + if rhs.where: + w = deepcopy(rhs.where) + w.relabel_aliases(change_map) + if not self.where: + # Since 'self' matches everything, add an explicit "include + # everything" where-constraint so that connections between the + # where clauses won't exclude valid results. + self.where.add(EverythingNode(), AND) + elif self.where: + # rhs has an empty where clause. + w = self.where_class() + w.add(EverythingNode(), AND) + else: + w = self.where_class() + self.where.add(w, connector) + + # Selection columns and extra extensions are those provided by 'rhs'. + self.select = [] + for col in rhs.select: + if isinstance(col, (list, tuple)): + self.select.append((change_map.get(col[0], col[0]), col[1])) + else: + item = deepcopy(col) + item.relabel_aliases(change_map) + self.select.append(item) + self.select_fields = rhs.select_fields[:] + self.extra_select = rhs.extra_select.copy() + self.extra_tables = rhs.extra_tables + self.extra_where = rhs.extra_where + self.extra_params = rhs.extra_params + + # Ordering uses the 'rhs' ordering, unless it has none, in which case + # the current ordering is used. + self.order_by = rhs.order_by and rhs.order_by[:] or self.order_by + self.extra_order_by = rhs.extra_order_by or self.extra_order_by + + def pre_sql_setup(self): + """ + Does any necessary class setup immediately prior to producing SQL. This + is for things that can't necessarily be done in __init__ because we + might not have all the pieces in place at that time. + """ + if not self.tables: + self.join((None, self.model._meta.db_table, None, None)) + if self.select_related and not self.related_select_cols: + self.fill_related_selections() + + def get_columns(self, with_aliases=False): + """ + Return the list of columns to use in the select statement. If no + columns have been specified, returns all columns relating to fields in + the model. + + If 'with_aliases' is true, any column names that are duplicated + (without the table names) are given unique aliases. This is needed in + some cases to avoid ambiguitity with nested queries. + """ + qn = self.quote_name_unless_alias + result = ['(%s) AS %s' % (col, alias) for alias, col in self.extra_select.iteritems()] + aliases = set(self.extra_select.keys()) + if with_aliases: + col_aliases = aliases.copy() + else: + col_aliases = set() + if self.select: + for col in self.select: + if isinstance(col, (list, tuple)): + r = '%s.%s' % (qn(col[0]), qn(col[1])) + if with_aliases and col[1] in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (r, c_alias)) + aliases.add(c_alias) + col_aliases.add(c_alias) + else: + result.append(r) + aliases.add(r) + col_aliases.add(col[1]) + else: + result.append(col.as_sql(quote_func=qn)) + if hasattr(col, 'alias'): + aliases.add(col.alias) + col_aliases.add(col.alias) + elif self.default_cols: + cols, new_aliases = self.get_default_columns(with_aliases, + col_aliases) + result.extend(cols) + aliases.update(new_aliases) + for table, col in self.related_select_cols: + r = '%s.%s' % (qn(table), qn(col)) + if with_aliases and col in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s AS %s' % (r, c_alias)) + aliases.add(c_alias) + col_aliases.add(c_alias) + else: + result.append(r) + aliases.add(r) + col_aliases.add(col) + + self._select_aliases = aliases + return result + + def get_default_columns(self, with_aliases=False, col_aliases=None): + """ + Computes the default columns for selecting every field in the base + model. + + Returns a list of strings, quoted appropriately for use in SQL + directly, as well as a set of aliases used in the select statement. + """ + result = [] + table_alias = self.tables[0] + root_pk = self.model._meta.pk.column + seen = {None: table_alias} + qn = self.quote_name_unless_alias + qn2 = self.connection.ops.quote_name + aliases = set() + for field, model in self.model._meta.get_fields_with_model(): + try: + alias = seen[model] + except KeyError: + alias = self.join((table_alias, model._meta.db_table, + root_pk, model._meta.pk.column)) + seen[model] = alias + if with_aliases and field.column in col_aliases: + c_alias = 'Col%d' % len(col_aliases) + result.append('%s.%s AS %s' % (qn(alias), + qn2(field.column), c_alias)) + col_aliases.add(c_alias) + aliases.add(c_alias) + else: + r = '%s.%s' % (qn(alias), qn2(field.column)) + result.append(r) + aliases.add(r) + if with_aliases: + col_aliases.add(field.column) + return result, aliases + + def get_from_clause(self): + """ + Returns a list of strings that are joined together to go after the + "FROM" part of the query, as well as a list any extra parameters that + need to be included. Sub-classes, can override this to create a + from-clause via a "select", for example (e.g. CountQuery). + + This should only be called after any SQL construction methods that + might change the tables we need. This means the select columns and + ordering must be done first. + """ + result = [] + qn = self.quote_name_unless_alias + qn2 = self.connection.ops.quote_name + first = True + for alias in self.tables: + if not self.alias_refcount[alias]: + continue + try: + name, alias, join_type, lhs, lhs_col, col, nullable = self.alias_map[alias] + except KeyError: + # Extra tables can end up in self.tables, but not in the + # alias_map if they aren't in a join. That's OK. We skip them. + continue + alias_str = (alias != name and ' %s' % alias or '') + if join_type and not first: + result.append('%s %s%s ON (%s.%s = %s.%s)' + % (join_type, qn(name), alias_str, qn(lhs), + qn2(lhs_col), qn(alias), qn2(col))) + else: + connector = not first and ', ' or '' + result.append('%s%s%s' % (connector, qn(name), alias_str)) + first = False + for t in self.extra_tables: + alias, unused = self.table_alias(t) + if alias not in self.alias_map: + connector = not first and ', ' or '' + result.append('%s%s' % (connector, qn(alias))) + first = False + return result, [] + + def get_grouping(self): + """ + Returns a tuple representing the SQL elements in the "group by" clause. + """ + qn = self.quote_name_unless_alias + result = [] + for col in self.group_by: + if isinstance(col, (list, tuple)): + result.append('%s.%s' % (qn(col[0]), qn(col[1]))) + elif hasattr(col, 'as_sql'): + result.append(col.as_sql(qn)) + else: + result.append(str(col)) + return result + + def get_ordering(self): + """ + Returns list representing the SQL elements in the "order by" clause. + Also sets the ordering_aliases attribute on this instance to a list of + extra aliases needed in the select. + + Determining the ordering SQL can change the tables we need to include, + so this should be run *before* get_from_clause(). + """ + if self.extra_order_by: + ordering = self.extra_order_by + elif not self.default_ordering: + ordering = [] + else: + ordering = self.order_by or self.model._meta.ordering + qn = self.quote_name_unless_alias + qn2 = self.connection.ops.quote_name + distinct = self.distinct + select_aliases = self._select_aliases + result = [] + ordering_aliases = [] + if self.standard_ordering: + asc, desc = ORDER_DIR['ASC'] + else: + asc, desc = ORDER_DIR['DESC'] + for field in ordering: + if field == '?': + result.append(self.connection.ops.random_function_sql()) + continue + if isinstance(field, int): + if field < 0: + order = desc + field = -field + else: + order = asc + result.append('%s %s' % (field, order)) + continue + if '.' in field: + # This came in through an extra(order_by=...) addition. Pass it + # on verbatim. + col, order = get_order_dir(field, asc) + table, col = col.split('.', 1) + elt = '%s.%s' % (qn(table), col) + if not distinct or elt in select_aliases: + result.append('%s %s' % (elt, order)) + elif get_order_dir(field)[0] not in self.extra_select: + # 'col' is of the form 'field' or 'field1__field2' or + # '-field1__field2__field', etc. + for table, col, order in self.find_ordering_name(field, + self.model._meta, default_order=asc): + elt = '%s.%s' % (qn(table), qn2(col)) + if distinct and elt not in select_aliases: + ordering_aliases.append(elt) + result.append('%s %s' % (elt, order)) + else: + col, order = get_order_dir(field, asc) + elt = qn(col) + if distinct and elt not in select_aliases: + ordering_aliases.append(elt) + result.append('%s %s' % (elt, order)) + self.ordering_aliases = ordering_aliases + return result + + def find_ordering_name(self, name, opts, alias=None, default_order='ASC', + already_seen=None): + """ + Returns the table alias (the name might be ambiguous, the alias will + not be) and column name for ordering by the given 'name' parameter. + The 'name' is of the form 'field1__field2__...__fieldN'. + """ + name, order = get_order_dir(name, default_order) + pieces = name.split(LOOKUP_SEP) + if not alias: + alias = self.get_initial_alias() + field, target, opts, joins, last = self.setup_joins(pieces, opts, + alias, False) + alias = joins[-1] + col = target.column + + # If we get to this point and the field is a relation to another model, + # append the default ordering for that model. + if field.rel and len(joins) > 1 and opts.ordering: + # Firstly, avoid infinite loops. + if not already_seen: + already_seen = set() + join_tuple = tuple([self.alias_map[j][TABLE_NAME] for j in joins]) + if join_tuple in already_seen: + raise FieldError('Infinite loop caused by ordering.') + already_seen.add(join_tuple) + + results = [] + for item in opts.ordering: + results.extend(self.find_ordering_name(item, opts, alias, + order, already_seen)) + return results + + if alias: + # We have to do the same "final join" optimisation as in + # add_filter, since the final column might not otherwise be part of + # the select set (so we can't order on it). + join = self.alias_map[alias] + if col == join[RHS_JOIN_COL]: + self.unref_alias(alias) + alias = join[LHS_ALIAS] + col = join[LHS_JOIN_COL] + return [(alias, col, order)] + + def table_alias(self, table_name, create=False): + """ + Returns a table alias for the given table_name and whether this is a + new alias or not. + + If 'create' is true, a new alias is always created. Otherwise, the + most recently created alias for the table (if one exists) is reused. + """ + current = self.table_map.get(table_name) + if not create and current: + alias = current[0] + self.alias_refcount[alias] += 1 + return alias, False + + # Create a new alias for this table. + if current: + alias = '%s%d' % (self.alias_prefix, len(self.alias_map) + 1) + current.append(alias) + else: + # The first occurence of a table uses the table name directly. + alias = table_name + self.table_map[alias] = [alias] + self.alias_refcount[alias] = 1 + #self.alias_map[alias] = None + self.tables.append(alias) + return alias, True + + def ref_alias(self, alias): + """ Increases the reference count for this alias. """ + self.alias_refcount[alias] += 1 + + def unref_alias(self, alias): + """ Decreases the reference count for this alias. """ + self.alias_refcount[alias] -= 1 + + def promote_alias(self, alias, unconditional=False): + """ + Promotes the join type of an alias to an outer join if it's possible + for the join to contain NULL values on the left. If 'unconditional' is + False, the join is only promoted if it is nullable, otherwise it is + always promoted. + """ + if ((unconditional or self.alias_map[alias][NULLABLE]) and + self.alias_map[alias] != self.LOUTER): + data = list(self.alias_map[alias]) + data[JOIN_TYPE] = self.LOUTER + self.alias_map[alias] = tuple(data) + + def change_aliases(self, change_map): + """ + Changes the aliases in change_map (which maps old-alias -> new-alias), + relabelling any references to them in select columns and the where + clause. + """ + assert set(change_map.keys()).intersection(set(change_map.values())) == set() + + # 1. Update references in "select" and "where". + self.where.relabel_aliases(change_map) + for pos, col in enumerate(self.select): + if isinstance(col, (list, tuple)): + self.select[pos] = (change_map.get(old_alias, old_alias), col[1]) + else: + col.relabel_aliases(change_map) + + # 2. Rename the alias in the internal table/alias datastructures. + for old_alias, new_alias in change_map.iteritems(): + alias_data = list(self.alias_map[old_alias]) + alias_data[RHS_ALIAS] = new_alias + + t = self.rev_join_map[old_alias] + data = list(self.join_map[t]) + data[data.index(old_alias)] = new_alias + self.join_map[t] = tuple(data) + self.rev_join_map[new_alias] = t + del self.rev_join_map[old_alias] + self.alias_refcount[new_alias] = self.alias_refcount[old_alias] + del self.alias_refcount[old_alias] + self.alias_map[new_alias] = tuple(alias_data) + del self.alias_map[old_alias] + + table_aliases = self.table_map[alias_data[TABLE_NAME]] + for pos, alias in enumerate(table_aliases): + if alias == old_alias: + table_aliases[pos] = new_alias + break + for pos, alias in enumerate(self.tables): + if alias == old_alias: + self.tables[pos] = new_alias + break + + # 3. Update any joins that refer to the old alias. + for alias, data in self.alias_map.iteritems(): + lhs = data[LHS_ALIAS] + if lhs in change_map: + data = list(data) + data[LHS_ALIAS] = change_map[lhs] + self.alias_map[alias] = tuple(data) + + def bump_prefix(self, exceptions=()): + """ + Changes the alias prefix to the next letter in the alphabet and + relabels all the aliases. Even tables that previously had no alias will + get an alias after this call (it's mostly used for nested queries and + the outer query will already be using the non-aliased table name). + + Subclasses who create their own prefix should override this method to + produce a similar result (a new prefix and relabelled aliases). + + The 'exceptions' parameter is a container that holds alias names which + should not be changed. + """ + assert ord(self.alias_prefix) < ord('Z') + self.alias_prefix = chr(ord(self.alias_prefix) + 1) + change_map = {} + prefix = self.alias_prefix + for pos, alias in enumerate(self.tables): + if alias in exceptions: + continue + new_alias = '%s%d' % (prefix, pos) + change_map[alias] = new_alias + self.tables[pos] = new_alias + self.change_aliases(change_map) + + def get_initial_alias(self): + """ + Returns the first alias for this query, after increasing its reference + count. + """ + if self.tables: + alias = self.tables[0] + self.ref_alias(alias) + else: + alias = self.join((None, self.model._meta.db_table, None, None)) + return alias + + def count_active_tables(self): + """ + Returns the number of tables in this query with a non-zero reference + count. + """ + return len([1 for count in self.alias_refcount.itervalues() if count]) + + def join(self, connection, always_create=False, exclusions=(), + promote=False, outer_if_first=False, nullable=False, reuse=None): + """ + Returns an alias for the join in 'connection', either reusing an + existing alias for that join or creating a new one. 'connection' is a + tuple (lhs, table, lhs_col, col) where 'lhs' is either an existing + table alias or a table name. The join correspods to the SQL equivalent + of:: + + lhs.lhs_col = table.col + + If 'always_create' is True and 'reuse' is None, a new alias is always + created, regardless of whether one already exists or not. Otherwise + 'reuse' must be a set and a new join is created unless one of the + aliases in `reuse` can be used. + + If 'exclusions' is specified, it is something satisfying the container + protocol ("foo in exclusions" must work) and specifies a list of + aliases that should not be returned, even if they satisfy the join. + + If 'promote' is True, the join type for the alias will be LOUTER (if + the alias previously existed, the join type will be promoted from INNER + to LOUTER, if necessary). + + If 'outer_if_first' is True and a new join is created, it will have the + LOUTER join type. This is used when joining certain types of querysets + and Q-objects together. + + If 'nullable' is True, the join can potentially involve NULL values and + is a candidate for promotion (to "left outer") when combining querysets. + """ + lhs, table, lhs_col, col = connection + if lhs in self.alias_map: + lhs_table = self.alias_map[lhs][TABLE_NAME] + else: + lhs_table = lhs + + if reuse and always_create and table in self.table_map: + # Convert the 'reuse' to case to be "exclude everything but the + # reusable set for this table". + exclusions = set(self.table_map[table]).difference(reuse) + always_create = False + t_ident = (lhs_table, table, lhs_col, col) + if not always_create: + for alias in self.join_map.get(t_ident, ()): + if alias not in exclusions: + self.ref_alias(alias) + if promote: + self.promote_alias(alias) + return alias + + # No reuse is possible, so we need a new alias. + alias, _ = self.table_alias(table, True) + if not lhs: + # Not all tables need to be joined to anything. No join type + # means the later columns are ignored. + join_type = None + elif promote or outer_if_first: + join_type = self.LOUTER + else: + join_type = self.INNER + join = (table, alias, join_type, lhs, lhs_col, col, nullable) + self.alias_map[alias] = join + if t_ident in self.join_map: + self.join_map[t_ident] += (alias,) + else: + self.join_map[t_ident] = (alias,) + self.rev_join_map[alias] = t_ident + return alias + + def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1, + used=None, requested=None, restricted=None): + """ + Fill in the information needed for a select_related query. The current + depth is measured as the number of connections away from the root model + (for example, cur_depth=1 means we are looking at models with direct + connections to the root model). + """ + if not restricted and self.max_depth and cur_depth > self.max_depth: + # We've recursed far enough; bail out. + return + if not opts: + opts = self.get_meta() + root_alias = self.get_initial_alias() + self.related_select_cols = [] + self.related_select_fields = [] + if not used: + used = set() + + # Setup for the case when only particular related fields should be + # included in the related selection. + if requested is None and restricted is not False: + if isinstance(self.select_related, dict): + requested = self.select_related + restricted = True + else: + restricted = False + + for f, model in opts.get_fields_with_model(): + if (not f.rel or (restricted and f.name not in requested) or + (not restricted and f.null) or f.rel.parent_link): + continue + table = f.rel.to._meta.db_table + if model: + int_opts = opts + alias = root_alias + for int_model in opts.get_base_chain(model): + lhs_col = int_opts.parents[int_model].column + int_opts = int_model._meta + alias = self.join((alias, int_opts.db_table, lhs_col, + int_opts.pk.column), exclusions=used, + promote=f.null) + else: + alias = root_alias + alias = self.join((alias, table, f.column, + f.rel.get_related_field().column), exclusions=used, + promote=f.null) + used.add(alias) + self.related_select_cols.extend([(alias, f2.column) + for f2 in f.rel.to._meta.fields]) + self.related_select_fields.extend(f.rel.to._meta.fields) + if restricted: + next = requested.get(f.name, {}) + else: + next = False + self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1, + used, next, restricted) + + def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, + can_reuse=None): + """ + Add a single filter to the query. The 'filter_expr' is a pair: + (filter_string, value). E.g. ('name__contains', 'fred') + + If 'negate' is True, this is an exclude() filter. If 'trim' is True, we + automatically trim the final join group (used internally when + constructing nested queries). + + If 'can_reuse' is a set, we are processing a component of a + multi-component filter (e.g. filter(Q1, Q2)). In this case, 'can_reuse' + will be a set of table aliases that can be reused in this filter, even + if we would otherwise force the creation of new aliases for a join + (needed for nested Q-filters). The set is updated by this method. + """ + arg, value = filter_expr + parts = arg.split(LOOKUP_SEP) + if not parts: + raise FieldError("Cannot parse keyword query %r" % arg) + + # Work out the lookup type and remove it from 'parts', if necessary. + if len(parts) == 1 or parts[-1] not in self.query_terms: + lookup_type = 'exact' + else: + lookup_type = parts.pop() + + # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all + # uses of None as a query value. + if value is None: + if lookup_type != 'exact': + raise ValueError("Cannot use None as a query value") + lookup_type = 'isnull' + value = True + elif callable(value): + value = value() + + opts = self.get_meta() + alias = self.get_initial_alias() + allow_many = trim or not negate + + try: + field, target, opts, join_list, last = self.setup_joins(parts, opts, + alias, True, allow_many, can_reuse=can_reuse) + except MultiJoin, e: + self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level])) + return + final = len(join_list) + penultimate = last.pop() + if penultimate == final: + penultimate = last.pop() + if trim and len(join_list) > 1: + extra = join_list[penultimate:] + join_list = join_list[:penultimate] + final = penultimate + penultimate = last.pop() + col = self.alias_map[extra[0]][LHS_JOIN_COL] + for alias in extra: + self.unref_alias(alias) + else: + col = target.column + alias = join_list[-1] + + if final > 1: + # An optimization: if the final join is against the same column as + # we are comparing against, we can go back one step in the join + # chain and compare against the lhs of the join instead. The result + # (potentially) involves one less table join. + join = self.alias_map[alias] + if col == join[RHS_JOIN_COL]: + self.unref_alias(alias) + alias = join[LHS_ALIAS] + col = join[LHS_JOIN_COL] + join_list = join_list[:-1] + final -= 1 + if final == penultimate: + penultimate = last.pop() + + if (lookup_type == 'isnull' and value is True and not negate and + final > 1): + # If the comparison is against NULL, we need to use a left outer + # join when connecting to the previous model. We make that + # adjustment here. We don't do this unless needed as it's less + # efficient at the database level. + self.promote_alias(join_list[penultimate]) + + if connector == OR: + # Some joins may need to be promoted when adding a new filter to a + # disjunction. We walk the list of new joins and where it diverges + # from any previous joins (ref count is 1 in the table list), we + # make the new additions (and any existing ones not used in the new + # join list) an outer join. + join_it = iter(join_list) + table_it = iter(self.tables) + join_it.next(), table_it.next() + for join in join_it: + table = table_it.next() + if join == table and self.alias_refcount[join] > 1: + continue + self.promote_alias(join) + if table != join: + self.promote_alias(table) + break + for join in join_it: + self.promote_alias(join) + for table in table_it: + # Some of these will have been promoted from the join_list, but + # that's harmless. + self.promote_alias(table) + + self.where.add((alias, col, field, lookup_type, value), connector) + if negate: + self.where.negate() + for alias in join_list: + self.promote_alias(alias) + if final > 1 and lookup_type != 'isnull': + for alias in join_list: + if self.alias_map[alias] == self.LOUTER: + j_col = self.alias_map[alias][RHS_JOIN_COL] + entry = Node([(alias, j_col, None, 'isnull', True)]) + entry.negate() + self.where.add(entry, AND) + break + if can_reuse is not None: + can_reuse.update(join_list) + + def add_q(self, q_object, used_aliases=None): + """ + Adds a Q-object to the current filter. + + Can also be used to add anything that has an 'add_to_query()' method. + """ + if used_aliases is None: + used_aliases = set() + if hasattr(q_object, 'add_to_query'): + # Complex custom objects are responsible for adding themselves. + q_object.add_to_query(self, used_aliases) + return + + if self.where and q_object.connector != AND and len(q_object) > 1: + self.where.start_subtree(AND) + subtree = True + else: + subtree = False + connector = AND + for child in q_object.children: + if isinstance(child, Node): + self.where.start_subtree(connector) + self.add_q(child, used_aliases) + self.where.end_subtree() + if q_object.negated: + self.where.children[-1].negate() + else: + self.add_filter(child, connector, q_object.negated, + can_reuse=used_aliases) + connector = q_object.connector + if subtree: + self.where.end_subtree() + + def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True, + allow_explicit_fk=False, can_reuse=None): + """ + Compute the necessary table joins for the passage through the fields + given in 'names'. 'opts' is the Options class for the current model + (which gives the table we are joining to), 'alias' is the alias for the + table we are joining to. If dupe_multis is True, any many-to-many or + many-to-one joins will always create a new alias (necessary for + disjunctive filters). + + Returns the final field involved in the join, the target database + column (used for any 'where' constraint), the final 'opts' value and the + list of tables joined. + """ + joins = [alias] + last = [0] + for pos, name in enumerate(names): + last.append(len(joins)) + if name == 'pk': + name = opts.pk.name + + try: + field, model, direct, m2m = opts.get_field_by_name(name) + except FieldDoesNotExist: + for f in opts.fields: + if allow_explicit_fk and name == f.attname: + # XXX: A hack to allow foo_id to work in values() for + # backwards compatibility purposes. If we dropped that + # feature, this could be removed. + field, model, direct, m2m = opts.get_field_by_name(f.name) + break + else: + names = opts.get_all_field_names() + raise FieldError("Cannot resolve keyword %r into field. " + "Choices are: %s" % (name, ", ".join(names))) + if not allow_many and (m2m or not direct): + for alias in joins: + self.unref_alias(alias) + raise MultiJoin(pos + 1) + if model: + # The field lives on a base class of the current model. + alias_list = [] + for int_model in opts.get_base_chain(model): + lhs_col = opts.parents[int_model].column + opts = int_model._meta + alias = self.join((alias, opts.db_table, lhs_col, + opts.pk.column), exclusions=joins) + joins.append(alias) + cached_data = opts._join_cache.get(name) + orig_opts = opts + + if direct: + if m2m: + # Many-to-many field defined on the current model. + if cached_data: + (table1, from_col1, to_col1, table2, from_col2, + to_col2, opts, target) = cached_data + else: + table1 = field.m2m_db_table() + from_col1 = opts.pk.column + to_col1 = field.m2m_column_name() + opts = field.rel.to._meta + table2 = opts.db_table + from_col2 = field.m2m_reverse_name() + to_col2 = opts.pk.column + target = opts.pk + orig_opts._join_cache[name] = (table1, from_col1, + to_col1, table2, from_col2, to_col2, opts, + target) + + int_alias = self.join((alias, table1, from_col1, to_col1), + dupe_multis, joins, nullable=True, reuse=can_reuse) + alias = self.join((int_alias, table2, from_col2, to_col2), + dupe_multis, joins, nullable=True, reuse=can_reuse) + joins.extend([int_alias, alias]) + elif field.rel: + # One-to-one or many-to-one field + if cached_data: + (table, from_col, to_col, opts, target) = cached_data + else: + opts = field.rel.to._meta + target = field.rel.get_related_field() + table = opts.db_table + from_col = field.column + to_col = target.column + orig_opts._join_cache[name] = (table, from_col, to_col, + opts, target) + + alias = self.join((alias, table, from_col, to_col), + exclusions=joins, nullable=field.null) + joins.append(alias) + else: + # Non-relation fields. + target = field + break + else: + orig_field = field + field = field.field + if m2m: + # Many-to-many field defined on the target model. + if cached_data: + (table1, from_col1, to_col1, table2, from_col2, + to_col2, opts, target) = cached_data + else: + table1 = field.m2m_db_table() + from_col1 = opts.pk.column + to_col1 = field.m2m_reverse_name() + opts = orig_field.opts + table2 = opts.db_table + from_col2 = field.m2m_column_name() + to_col2 = opts.pk.column + target = opts.pk + orig_opts._join_cache[name] = (table1, from_col1, + to_col1, table2, from_col2, to_col2, opts, + target) + + int_alias = self.join((alias, table1, from_col1, to_col1), + dupe_multis, joins, nullable=True, reuse=can_reuse) + alias = self.join((int_alias, table2, from_col2, to_col2), + dupe_multis, joins, nullable=True, reuse=can_reuse) + joins.extend([int_alias, alias]) + else: + # One-to-many field (ForeignKey defined on the target model) + if cached_data: + (table, from_col, to_col, opts, target) = cached_data + else: + local_field = opts.get_field_by_name( + field.rel.field_name)[0] + opts = orig_field.opts + table = opts.db_table + from_col = local_field.column + to_col = field.column + target = opts.pk + orig_opts._join_cache[name] = (table, from_col, to_col, + opts, target) + + alias = self.join((alias, table, from_col, to_col), + dupe_multis, joins, nullable=True, reuse=can_reuse) + joins.append(alias) + + if pos != len(names) - 1: + raise FieldError("Join on field %r not permitted." % name) + + return field, target, opts, joins, last + + def split_exclude(self, filter_expr, prefix): + """ + When doing an exclude against any kind of N-to-many relation, we need + to use a subquery. This method constructs the nested query, given the + original exclude filter (filter_expr) and the portion up to the first + N-to-many relation field. + """ + query = Query(self.model, self.connection) + query.add_filter(filter_expr) + query.set_start(prefix) + query.clear_ordering(True) + self.add_filter(('%s__in' % prefix, query), negate=True, trim=True) + + def set_limits(self, low=None, high=None): + """ + Adjusts the limits on the rows retrieved. We use low/high to set these, + as it makes it more Pythonic to read and write. When the SQL query is + created, they are converted to the appropriate offset and limit values. + + Any limits passed in here are applied relative to the existing + constraints. So low is added to the current low value and both will be + clamped to any existing high value. + """ + if high: + if self.high_mark: + self.high_mark = min(self.high_mark, self.low_mark + high) + else: + self.high_mark = self.low_mark + high + if low: + if self.high_mark: + self.low_mark = min(self.high_mark, self.low_mark + low) + else: + self.low_mark = self.low_mark + low + + def clear_limits(self): + """ + Clears any existing limits. + """ + self.low_mark, self.high_mark = 0, None + + def can_filter(self): + """ + Returns True if adding filters to this instance is still possible. + + Typically, this means no limits or offsets have been put on the results. + """ + return not (self.low_mark or self.high_mark) + + def add_fields(self, field_names, allow_m2m=True): + """ + Adds the given (model) fields to the select set. The field names are + added in the order specified. + """ + alias = self.get_initial_alias() + opts = self.get_meta() + try: + for name in field_names: + field, target, u2, joins, u3 = self.setup_joins( + name.split(LOOKUP_SEP), opts, alias, False, allow_m2m, + True) + final_alias = joins[-1] + col = target.column + if len(joins) > 1: + join = self.alias_map[final_alias] + if col == join[RHS_JOIN_COL]: + self.unref_alias(final_alias) + final_alias = join[LHS_ALIAS] + col = join[LHS_JOIN_COL] + joins = joins[:-1] + for join in joins[1:]: + # Only nullable aliases are promoted, so we don't end up + # doing unnecessary left outer joins here. + self.promote_alias(join) + self.select.append((final_alias, col)) + self.select_fields.append(field) + except MultiJoin: + raise FieldError("Invalid field name: '%s'" % name) + except FieldError: + names = opts.get_all_field_names() + self.extra_select.keys() + names.sort() + raise FieldError("Cannot resolve keyword %r into field. " + "Choices are: %s" % (name, ", ".join(names))) + + def add_ordering(self, *ordering): + """ + Adds items from the 'ordering' sequence to the query's "order by" + clause. These items are either field names (not column names) -- + possibly with a direction prefix ('-' or '?') -- or ordinals, + corresponding to column positions in the 'select' list. + + If 'ordering' is empty, all ordering is cleared from the query. + """ + errors = [] + for item in ordering: + if not ORDER_PATTERN.match(item): + errors.append(item) + if errors: + raise FieldError('Invalid order_by arguments: %s' % errors) + if ordering: + self.order_by.extend(ordering) + else: + self.default_ordering = False + + def clear_ordering(self, force_empty=False): + """ + Removes any ordering settings. If 'force_empty' is True, there will be + no ordering in the resulting query (not even the model's default). + """ + self.order_by = [] + self.extra_order_by = () + if force_empty: + self.default_ordering = False + + def add_count_column(self): + """ + Converts the query to do count(...) or count(distinct(pk)) in order to + get its size. + """ + # TODO: When group_by support is added, this needs to be adjusted so + # that it doesn't totally overwrite the select list. + if not self.distinct: + if not self.select: + select = Count() + else: + assert len(self.select) == 1, \ + "Cannot add count col with multiple cols in 'select': %r" % self.select + select = Count(self.select[0]) + else: + opts = self.model._meta + if not self.select: + select = Count((self.join((None, opts.db_table, None, None)), + opts.pk.column), True) + else: + # Because of SQL portability issues, multi-column, distinct + # counts need a sub-query -- see get_count() for details. + assert len(self.select) == 1, \ + "Cannot add count col with multiple cols in 'select'." + select = Count(self.select[0], True) + + # Distinct handling is done in Count(), so don't do it at this + # level. + self.distinct = False + self.select = [select] + self.select_fields = [None] + self.extra_select = {} + self.extra_select_params = () + + def add_select_related(self, fields): + """ + Sets up the select_related data structure so that we only select + certain related models (as opposed to all models, when + self.select_related=True). + """ + field_dict = {} + for field in fields: + d = field_dict + for part in field.split(LOOKUP_SEP): + d = d.setdefault(part, {}) + self.select_related = field_dict + self.related_select_cols = [] + self.related_select_fields = [] + + def add_extra(self, select, select_params, where, params, tables, order_by): + """ + Adds data to the various extra_* attributes for user-created additions + to the query. + """ + if select: + # The extra select might be ordered (because it will be accepting + # parameters). + if (isinstance(select, SortedDict) and + not isinstance(self.extra_select, SortedDict)): + self.extra_select = SortedDict(self.extra_select) + self.extra_select.update(select) + if select_params: + self.extra_select_params += tuple(select_params) + if where: + self.extra_where += tuple(where) + if params: + self.extra_params += tuple(params) + if tables: + self.extra_tables += tuple(tables) + if order_by: + self.extra_order_by = order_by + + def trim_extra_select(self, names): + """ + Removes any aliases in the extra_select dictionary that aren't in + 'names'. + + This is needed if we are selecting certain values that don't incldue + all of the extra_select names. + """ + for key in set(self.extra_select).difference(set(names)): + del self.extra_select[key] + + def set_start(self, start): + """ + Sets the table from which to start joining. The start position is + specified by the related attribute from the base model. This will + automatically set to the select column to be the column linked from the + previous table. + + This method is primarily for internal use and the error checking isn't + as friendly as add_filter(). Mostly useful for querying directly + against the join table of many-to-many relation in a subquery. + """ + opts = self.model._meta + alias = self.get_initial_alias() + field, col, opts, joins, last = self.setup_joins( + start.split(LOOKUP_SEP), opts, alias, False) + alias = joins[last[-1]] + self.select = [(alias, self.alias_map[alias][RHS_JOIN_COL])] + self.select_fields = [field] + self.start_meta = opts + + # The call to setup_joins add an extra reference to everything in + # joins. So we need to unref everything once, and everything prior to + # the final join a second time. + for alias in joins: + self.unref_alias(alias) + for alias in joins[:last[-1]]: + self.unref_alias(alias) + + def execute_sql(self, result_type=MULTI): + """ + Run the query against the database and returns the result(s). The + return value is a single data item if result_type is SINGLE, or an + iterator over the results if the result_type is MULTI. + + result_type is either MULTI (use fetchmany() to retrieve all rows), + SINGLE (only retrieve a single row), or None (no results expected, but + the cursor is returned, since it's used by subclasses such as + InsertQuery). + """ + try: + sql, params = self.as_sql() + if not sql: + raise EmptyResultSet + except EmptyResultSet: + if result_type == MULTI: + return empty_iter() + else: + return + + cursor = self.connection.cursor() + cursor.execute(sql, params) + + if not result_type: + return cursor + if result_type == SINGLE: + if self.ordering_aliases: + return cursor.fetchone()[:-len(results.ordering_aliases)] + return cursor.fetchone() + + # The MULTI case. + if self.ordering_aliases: + return order_modified_iter(cursor, len(self.ordering_aliases), + self.connection.features.empty_fetchmany_value) + return iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)), + self.connection.features.empty_fetchmany_value) + +# Use the backend's custom Query class if it defines one. Otherwise, use the +# default. +if connection.features.uses_custom_query_class: + Query = connection.ops.query_class(Query) + +def get_order_dir(field, default='ASC'): + """ + Returns the field name and direction for an order specification. For + example, '-foo' is returned as ('foo', 'DESC'). + + The 'default' param is used to indicate which way no prefix (or a '+' + prefix) should sort. The '-' prefix always sorts the opposite way. + """ + dirn = ORDER_DIR[default] + if field[0] == '-': + return field[1:], dirn[1] + return field, dirn[0] + +def empty_iter(): + """ + Returns an iterator containing no results. + """ + yield iter([]).next() + +def order_modified_iter(cursor, trim, sentinel): + """ + Yields blocks of rows from a cursor. We use this iterator in the special + case when extra output columns have been added to support ordering + requirements. We must trim those extra columns before anything else can use + the results, since they're only needed to make the SQL valid. + """ + for rows in iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)), + sentinel): + yield [r[:-trim] for r in rows] + +def setup_join_cache(sender): + """ + The information needed to join between model fields is something that is + invariant over the life of the model, so we cache it in the model's Options + class, rather than recomputing it all the time. + + This method initialises the (empty) cache when the model is created. + """ + sender._meta._join_cache = {} + +dispatcher.connect(setup_join_cache, signal=signals.class_prepared) + diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py new file mode 100644 index 0000000000..382e6e94ff --- /dev/null +++ b/django/db/models/sql/subqueries.py @@ -0,0 +1,367 @@ +""" +Query subclasses which provide extra functionality beyond simple data retrieval. +""" + +from django.contrib.contenttypes import generic +from django.core.exceptions import FieldError +from django.db.models.sql.constants import * +from django.db.models.sql.datastructures import RawValue, Date +from django.db.models.sql.query import Query +from django.db.models.sql.where import AND + +__all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', + 'CountQuery'] + +class DeleteQuery(Query): + """ + Delete queries are done through this class, since they are more constrained + than general queries. + """ + def as_sql(self): + """ + Creates the SQL for this query. Returns the SQL string and list of + parameters. + """ + assert len(self.tables) == 1, \ + "Can only delete from one table at a time." + result = ['DELETE FROM %s' % self.quote_name_unless_alias(self.tables[0])] + where, params = self.where.as_sql() + result.append('WHERE %s' % where) + return ' '.join(result), tuple(params) + + def do_query(self, table, where): + self.tables = [table] + self.where = where + self.execute_sql(None) + + def delete_batch_related(self, pk_list): + """ + Set up and execute delete queries for all the objects related to the + primary key values in pk_list. To delete the objects themselves, use + the delete_batch() method. + + More than one physical query may be executed if there are a + lot of values in pk_list. + """ + cls = self.model + for related in cls._meta.get_all_related_many_to_many_objects(): + if not isinstance(related.field, generic.GenericRelation): + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + where = self.where_class() + where.add((None, related.field.m2m_reverse_name(), + related.field, 'in', + pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]), + AND) + self.do_query(related.field.m2m_db_table(), where) + + for f in cls._meta.many_to_many: + w1 = self.where_class() + if isinstance(f, generic.GenericRelation): + from django.contrib.contenttypes.models import ContentType + field = f.rel.to._meta.get_field(f.content_type_field_name) + w1.add((None, field.column, field, 'exact', + ContentType.objects.get_for_model(cls).id), AND) + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + where = self.where_class() + where.add((None, f.m2m_column_name(), f, 'in', + pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), + AND) + if w1: + where.add(w1, AND) + self.do_query(f.m2m_db_table(), where) + + def delete_batch(self, pk_list): + """ + Set up and execute delete queries for all the objects in pk_list. This + should be called after delete_batch_related(), if necessary. + + More than one physical query may be executed if there are a + lot of values in pk_list. + """ + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + where = self.where_class() + field = self.model._meta.pk + where.add((None, field.column, field, 'in', + pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND) + self.do_query(self.model._meta.db_table, where) + +class UpdateQuery(Query): + """ + Represents an "update" SQL query. + """ + def __init__(self, *args, **kwargs): + super(UpdateQuery, self).__init__(*args, **kwargs) + self._setup_query() + + def _setup_query(self): + """ + Runs on initialisation and after cloning. Any attributes that would + normally be set in __init__ should go in here, instead, so that they + are also set up after a clone() call. + """ + self.values = [] + self.related_ids = None + if not hasattr(self, 'related_updates'): + self.related_updates = {} + + def clone(self, klass=None, **kwargs): + return super(UpdateQuery, self).clone(klass, + related_updates=self.related_updates.copy, **kwargs) + + def execute_sql(self, result_type=None): + super(UpdateQuery, self).execute_sql(result_type) + for query in self.get_related_updates(): + query.execute_sql(result_type) + + def as_sql(self): + """ + Creates the SQL for this query. Returns the SQL string and list of + parameters. + """ + self.pre_sql_setup() + if not self.values: + return '', () + table = self.tables[0] + qn = self.quote_name_unless_alias + result = ['UPDATE %s' % qn(table)] + result.append('SET') + values, update_params = [], [] + for name, val, placeholder in self.values: + if val is not None: + values.append('%s = %s' % (qn(name), placeholder)) + update_params.append(val) + else: + values.append('%s = NULL' % qn(name)) + result.append(', '.join(values)) + where, params = self.where.as_sql() + if where: + result.append('WHERE %s' % where) + return ' '.join(result), tuple(update_params + params) + + def pre_sql_setup(self): + """ + If the update depends on results from other tables, we need to do some + munging of the "where" conditions to match the format required for + (portable) SQL updates. That is done here. + + Further, if we are going to be running multiple updates, we pull out + the id values to update at this point so that they don't change as a + result of the progressive updates. + """ + self.select_related = False + self.clear_ordering(True) + super(UpdateQuery, self).pre_sql_setup() + count = self.count_active_tables() + if not self.related_updates and count == 1: + return + + # We need to use a sub-select in the where clause to filter on things + # from other tables. + query = self.clone(klass=Query) + query.bump_prefix() + query.select = [] + query.extra_select = {} + query.add_fields([query.model._meta.pk.name]) + + # Now we adjust the current query: reset the where clause and get rid + # of all the tables we don't need (since they're in the sub-select). + self.where = self.where_class() + if self.related_updates: + idents = [] + for rows in query.execute_sql(MULTI): + idents.extend([r[0] for r in rows]) + self.add_filter(('pk__in', idents)) + self.related_ids = idents + else: + self.add_filter(('pk__in', query)) + for alias in self.tables[1:]: + self.alias_refcount[alias] = 0 + + def clear_related(self, related_field, pk_list): + """ + Set up and execute an update query that clears related entries for the + keys in pk_list. + + This is used by the QuerySet.delete_objects() method. + """ + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + self.where = self.where_class() + f = self.model._meta.pk + self.where.add((None, f.column, f, 'in', + pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), + AND) + self.values = [(related_field.column, None, '%s')] + self.execute_sql(None) + + def add_update_values(self, values): + """ + Convert a dictionary of field name to value mappings into an update + query. This is the entry point for the public update() method on + querysets. + """ + values_seq = [] + for name, val in values.iteritems(): + field, model, direct, m2m = self.model._meta.get_field_by_name(name) + if not direct or m2m: + raise FieldError('Cannot update model field %r (only non-relations and foreign keys permitted).' % field) + values_seq.append((field, model, val)) + return self.add_update_fields(values_seq) + + def add_update_fields(self, values_seq): + """ + Turn a sequence of (field, model, value) triples into an update query. + Used by add_update_values() as well as the "fast" update path when + saving models. + """ + from django.db.models.base import Model + for field, model, val in values_seq: + # FIXME: Some sort of db_prep_* is probably more appropriate here. + if field.rel and isinstance(val, Model): + val = val.pk + + # Getting the placeholder for the field. + if hasattr(field, 'get_placeholder'): + placeholder = field.get_placeholder(val) + else: + placeholder = '%s' + + if model: + self.add_related_update(model, field.column, val, placeholder) + else: + self.values.append((field.column, val, placeholder)) + + def add_related_update(self, model, column, value, placeholder): + """ + Adds (name, value) to an update query for an ancestor model. + + Updates are coalesced so that we only run one update query per ancestor. + """ + try: + self.related_updates[model].append((column, value, placeholder)) + except KeyError: + self.related_updates[model] = [(column, value, placeholder)] + + def get_related_updates(self): + """ + Returns a list of query objects: one for each update required to an + ancestor model. Each query will have the same filtering conditions as + the current query but will only update a single table. + """ + if not self.related_updates: + return [] + result = [] + for model, values in self.related_updates.iteritems(): + query = UpdateQuery(model, self.connection) + query.values = values + if self.related_ids: + query.add_filter(('pk__in', self.related_ids)) + result.append(query) + return result + +class InsertQuery(Query): + def __init__(self, *args, **kwargs): + super(InsertQuery, self).__init__(*args, **kwargs) + self.columns = [] + self.values = [] + self.params = () + + def clone(self, klass=None, **kwargs): + extras = {'columns': self.columns[:], 'values': self.values[:], + 'params': self.params} + return super(InsertQuery, self).clone(klass, extras) + + def as_sql(self): + # We don't need quote_name_unless_alias() here, since these are all + # going to be column names (so we can avoid the extra overhead). + qn = self.connection.ops.quote_name + result = ['INSERT INTO %s' % qn(self.model._meta.db_table)] + result.append('(%s)' % ', '.join([qn(c) for c in self.columns])) + result.append('VALUES (%s)' % ', '.join(self.values)) + return ' '.join(result), self.params + + def execute_sql(self, return_id=False): + cursor = super(InsertQuery, self).execute_sql(None) + if return_id: + return self.connection.ops.last_insert_id(cursor, + self.model._meta.db_table, self.model._meta.pk.column) + + def insert_values(self, insert_values, raw_values=False): + """ + Set up the insert query from the 'insert_values' dictionary. The + dictionary gives the model field names and their target values. + + If 'raw_values' is True, the values in the 'insert_values' dictionary + are inserted directly into the query, rather than passed as SQL + parameters. This provides a way to insert NULL and DEFAULT keywords + into the query, for example. + """ + placeholders, values = [], [] + for field, val in insert_values: + if hasattr(field, 'get_placeholder'): + # Some fields (e.g. geo fields) need special munging before + # they can be inserted. + placeholders.append(field.get_placeholder(val)) + else: + placeholders.append('%s') + + self.columns.append(field.column) + values.append(val) + if raw_values: + self.values.extend(values) + else: + self.params += tuple(values) + self.values.extend(placeholders) + +class DateQuery(Query): + """ + A DateQuery is a normal query, except that it specifically selects a single + date field. This requires some special handling when converting the results + back to Python objects, so we put it in a separate class. + """ + def results_iter(self): + """ + Returns an iterator over the results from executing this query. + """ + resolve_columns = hasattr(self, 'resolve_columns') + if resolve_columns: + from django.db.models.fields import DateTimeField + fields = [DateTimeField()] + else: + from django.db.backends.util import typecast_timestamp + needs_string_cast = self.connection.features.needs_datetime_string_cast + + offset = len(self.extra_select) + for rows in self.execute_sql(MULTI): + for row in rows: + date = row[offset] + if resolve_columns: + date = self.resolve_columns([date], fields)[0] + elif needs_string_cast: + date = typecast_timestamp(str(date)) + yield date + + def add_date_select(self, column, lookup_type, order='ASC'): + """ + Converts the query into a date extraction query. + """ + alias = self.join((None, self.model._meta.db_table, None, None)) + select = Date((alias, column), lookup_type, + self.connection.ops.date_trunc_sql) + self.select = [select] + self.select_fields = [None] + self.distinct = True + self.order_by = order == 'ASC' and [1] or [-1] + +class CountQuery(Query): + """ + A CountQuery knows how to take a normal query which would select over + multiple distinct columns and turn it into SQL that can be used on a + variety of backends (it requires a select in the FROM clause). + """ + def get_from_clause(self): + result, params = self._query.as_sql() + return ['(%s) A1' % result], params + + def get_ordering(self): + return () + diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py new file mode 100644 index 0000000000..c8857a01fe --- /dev/null +++ b/django/db/models/sql/where.py @@ -0,0 +1,171 @@ +""" +Code to manage the creation and SQL rendering of 'where' constraints. +""" +import datetime + +from django.utils import tree +from django.db import connection +from django.db.models.fields import Field +from django.db.models.query_utils import QueryWrapper +from datastructures import EmptyResultSet, FullResultSet + +# Connection types +AND = 'AND' +OR = 'OR' + +class WhereNode(tree.Node): + """ + Used to represent the SQL where-clause. + + The class is tied to the Query class that created it (in order to create + the corret SQL). + + The children in this tree are usually either Q-like objects or lists of + [table_alias, field_name, field_class, lookup_type, value]. However, a + child could also be any class with as_sql() and relabel_aliases() methods. + """ + default = AND + + def as_sql(self, node=None, qn=None): + """ + Returns the SQL version of the where clause and the value to be + substituted in. Returns None, None if this node is empty. + + If 'node' is provided, that is the root of the SQL generation + (generally not needed except by the internal implementation for + recursion). + """ + if node is None: + node = self + if not qn: + qn = connection.ops.quote_name + if not node.children: + return None, [] + result = [] + result_params = [] + empty = True + for child in node.children: + try: + if hasattr(child, 'as_sql'): + sql, params = child.as_sql(qn=qn) + format = '(%s)' + elif isinstance(child, tree.Node): + sql, params = self.as_sql(child, qn) + if len(child.children) == 1: + format = '%s' + else: + format = '(%s)' + if child.negated: + format = 'NOT %s' % format + else: + sql, params = self.make_atom(child, qn) + format = '%s' + except EmptyResultSet: + if node.connector == AND and not node.negated: + # We can bail out early in this particular case (only). + raise + elif node.negated: + empty = False + continue + except FullResultSet: + if self.connector == OR: + if node.negated: + empty = True + break + # We match everything. No need for any constraints. + return '', [] + if node.negated: + empty = True + continue + empty = False + if sql: + result.append(format % sql) + result_params.extend(params) + if empty: + raise EmptyResultSet + conn = ' %s ' % node.connector + return conn.join(result), result_params + + def make_atom(self, child, qn): + """ + Turn a tuple (table_alias, field_name, field_class, lookup_type, value) + into valid SQL. + + Returns the string for the SQL fragment and the parameters to use for + it. + """ + table_alias, name, field, lookup_type, value = child + if table_alias: + lhs = '%s.%s' % (qn(table_alias), qn(name)) + else: + lhs = qn(name) + db_type = field and field.db_type() or None + field_sql = connection.ops.field_cast_sql(db_type) % lhs + + if isinstance(value, datetime.datetime): + cast_sql = connection.ops.datetime_cast_sql() + else: + cast_sql = '%s' + + if field: + params = field.get_db_prep_lookup(lookup_type, value) + else: + params = Field().get_db_prep_lookup(lookup_type, value) + if isinstance(params, QueryWrapper): + extra, params = params.data + else: + extra = '' + + if lookup_type in connection.operators: + format = "%s %%s %s" % (connection.ops.lookup_cast(lookup_type), + extra) + return (format % (field_sql, + connection.operators[lookup_type] % cast_sql), params) + + if lookup_type == 'in': + if not value: + raise EmptyResultSet + if extra: + return ('%s IN %s' % (field_sql, extra), params) + return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(value))), + params) + elif lookup_type in ('range', 'year'): + return ('%s BETWEEN %%s and %%s' % field_sql, params) + elif lookup_type in ('month', 'day'): + return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type, + field_sql), params) + elif lookup_type == 'isnull': + return ('%s IS %sNULL' % (field_sql, (not value and 'NOT ' or '')), + params) + elif lookup_type == 'search': + return (connection.ops.fulltext_search_sql(field_sql), params) + elif lookup_type in ('regex', 'iregex'): + return connection.ops.regex_lookup(lookup_type) % (field_sql, cast_sql), params + + raise TypeError('Invalid lookup_type: %r' % lookup_type) + + def relabel_aliases(self, change_map, node=None): + """ + Relabels the alias values of any children. 'change_map' is a dictionary + mapping old (current) alias values to the new values. + """ + if not node: + node = self + for pos, child in enumerate(node.children): + if hasattr(child, 'relabel_aliases'): + child.relabel_aliases(change_map) + elif isinstance(child, tree.Node): + self.relabel_aliases(change_map, child) + else: + if child[0] in change_map: + node.children[pos] = (change_map[child[0]],) + child[1:] + +class EverythingNode(object): + """ + A node that matches everything. + """ + def as_sql(self, qn=None): + raise FullResultSet + + def relabel_aliases(self, change_map, node=None): + return diff --git a/django/utils/tree.py b/django/utils/tree.py new file mode 100644 index 0000000000..a62a4ae6c3 --- /dev/null +++ b/django/utils/tree.py @@ -0,0 +1,134 @@ +""" +A class for storing a tree graph. Primarily used for filter constructs in the +ORM. +""" + +from copy import deepcopy + +class Node(object): + """ + A single internal node in the tree graph. A Node should be viewed as a + connection (the root) with the children being either leaf nodes or other + Node instances. + """ + # Standard connector type. Clients usually won't use this at all and + # subclasses will usually override the value. + default = 'DEFAULT' + + def __init__(self, children=None, connector=None, negated=False): + """ + Constructs a new Node. If no connector is given, the default will be + used. + + Warning: You probably don't want to pass in the 'negated' parameter. It + is NOT the same as constructing a node and calling negate() on the + result. + """ + self.children = children and children[:] or [] + self.connector = connector or self.default + self.subtree_parents = [] + self.negated = negated + + def __str__(self): + if self.negated: + return '(NOT (%s: %s))' % (self.connector, ', '.join([str(c) for c + in self.children])) + return '(%s: %s)' % (self.connector, ', '.join([str(c) for c in + self.children])) + + def __deepcopy__(self, memodict): + """ + Utility method used by copy.deepcopy(). + """ + obj = Node(connector=self.connector, negated=self.negated) + obj.__class__ = self.__class__ + obj.children = deepcopy(self.children, memodict) + obj.subtree_parents = deepcopy(self.subtree_parents, memodict) + return obj + + def __len__(self): + """ + The size of a node if the number of children it has. + """ + return len(self.children) + + def __nonzero__(self): + """ + For truth value testing. + """ + return bool(self.children) + + def __contains__(self, other): + """ + Returns True is 'other' is a direct child of this instance. + """ + return other in self.children + + def add(self, node, conn_type): + """ + Adds a new node to the tree. If the conn_type is the same as the root's + current connector type, the node is added to the first level. + Otherwise, the whole tree is pushed down one level and a new root + connector is created, connecting the existing tree and the new node. + """ + if node in self.children: + return + if len(self.children) < 2: + self.connector = conn_type + if self.connector == conn_type: + if isinstance(node, Node) and (node.connector == conn_type or + len(node) == 1): + self.children.extend(node.children) + else: + self.children.append(node) + else: + obj = Node(self.children, self.connector, self.negated) + self.connector = conn_type + self.children = [obj, node] + + def negate(self): + """ + Negate the sense of the root connector. This reorganises the children + so that the current node has a single child: a negated node containing + all the previous children. This slightly odd construction makes adding + new children behave more intuitively. + + Interpreting the meaning of this negate is up to client code. This + method is useful for implementing "not" arrangements. + """ + self.children = [Node(self.children, self.connector, not self.negated)] + self.connector = self.default + + def start_subtree(self, conn_type): + """ + Sets up internal state so that new nodes are added to a subtree of the + current node. The conn_type specifies how the sub-tree is joined to the + existing children. + """ + if len(self.children) == 1: + self.connector = conn_type + elif self.connector != conn_type: + self.children = [Node(self.children, self.connector, self.negated)] + self.connector = conn_type + self.negated = False + + self.subtree_parents.append(Node(self.children, self.connector, + self.negated)) + self.connector = self.default + self.negated = False + self.children = [] + + def end_subtree(self): + """ + Closes off the most recently unmatched start_subtree() call. + + This puts the current state into a node of the parent tree and returns + the current instances state to be the parent. + """ + obj = self.subtree_parents.pop() + node = Node(self.children, self.connector) + self.connector = obj.connector + self.negated = obj.negated + self.children = obj.children + self.children.append(node) + diff --git a/docs/db-api.txt b/docs/db-api.txt index f8ed16988e..6585890c72 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -160,37 +160,6 @@ When you save an object, Django performs the following steps: is used to provide notification that an object has been successfully saved. (These signals are not yet documented.) -Raw saves -~~~~~~~~~ - -**New in Django development version** - -The pre-processing step (#2 in the previous section) is useful, but it modifies -the data stored in a field. This can cause problems if you're relying upon the -data you provide being used as-is. - -For example, if you're setting up conditions for a test, you'll want the test -conditions to be repeatable. If pre-processing is performed, the data used -to specify test conditions may be modified, changing the conditions for the -test each time the test is run. - -In cases such as this, you need to prevent pre-processing from being performed -when you save an object. To do this, you can invoke a **raw save** by passing -``raw=True`` as an argument to the ``save()`` method:: - - b4.save(raw=True) # Save object, but do no pre-processing - -A raw save skips the usual data pre-processing that is performed during the -save. All other steps in the save (pre-save signal, data preparation, data -insertion, and post-save signal) are performed as normal. - -.. admonition:: When to use a raw save - - Generally speaking, you shouldn't need to use a raw save. Disabling field - pre-processing is an extraordinary measure that should only be required - in extraordinary circumstances, such as setting up reliable test - conditions. - Saving changes to objects ========================= @@ -211,11 +180,11 @@ Saving ForeignKey and ManyToManyField fields -------------------------------------------- Updating ``ForeignKey`` fields works exactly the same way as saving a normal -field; simply assign an object of the right type to the field in question:: +field; simply assign an object of the right type to the field in question:: - cheese_blog = Blog.objects.get(name="Cheddar Talk") - entry.blog = cheese_blog - entry.save() + cheese_blog = Blog.objects.get(name="Cheddar Talk") + entry.blog = cheese_blog + entry.save() Updating a ``ManyToManyField`` works a little differently; use the ``add()`` method on the field to add a record to the relation:: @@ -422,6 +391,14 @@ This returns the sixth through tenth objects (``OFFSET 5 LIMIT 5``):: Entry.objects.all()[5:10] +You can also slice from the item ''N'' to the end of the queryset. For +example, to return everything from the sixth item onwards:: + + Entry.objects.all()[5:] + +How this last example is implemented in SQL varies depending upon the database +used, but it is supported in all cases. + Generally, slicing a ``QuerySet`` returns a new ``QuerySet`` -- it doesn't evaluate the query. An exception is if you use the "step" parameter of Python slice syntax. For example, this would actually execute the query in order to @@ -514,15 +491,70 @@ like so:: Note: ``order_by('?')`` queries may be expensive and slow, depending on the database backend you're using. -To order by a field in a different table, add the other table's name and a dot, -like so:: +To order by a field in a different model, use the same syntax as when you are +querying across model relations. That is, the name of the field, followed by a +double underscore (``__``), followed by the name of the field in the new model, +and so on for as many models as you want to join. For example:: - Entry.objects.order_by('blogs_blog.name', 'headline') + Entry.objects.order_by('blog__name', 'headline') + +If you try to order by a field that is a relation to another model, Django will +use the default ordering on the related model (or order by the related model's +primary key if there is no ``Meta.ordering`` specified. For example:: + + Entry.objects.order_by('blog') + +...is identical to:: + + Entry.objects.order_by('blog__id') + +...since the ``Blog`` model has no default ordering specified. + +Be cautious when ordering by fields in related models if you are also using +``distinct()``. See the note in the `distinct()`_ section for an explanation +of how related model ordering can change the expected results. + +It is permissible to specify a multi-valued field to order the results by (for +example, a ``ManyToMany`` field). Normally this won't be a sensible thing to +do and it's really an advanced usage feature. However, if you know that your +queryset's filtering or available data implies that there will only be one +ordering piece of data for each of the main items you are selecting, the +ordering may well be exactly what you want to do. Use ordering on multi-valued +fields with care and make sure the results are what you expect. + +**New in Django development version:** If you don't want any ordering to be +applied to a query, not even the default ordering, call ``order_by()`` with no +parameters. + +**New in Django development version:** The syntax for ordering across related +models has changed. See the `Django 0.96 documentation`_ for the old behaviour. + +.. _Django 0.96 documentation: http://www.djangoproject.com/documentation/0.96/model-api/#floatfield There's no way to specify whether ordering should be case sensitive. With respect to case-sensitivity, Django will order results however your database backend normally orders them. +``reverse()`` +~~~~~~~~~~~~~ + +**New in Django development version** + +If you want to reverse the order in which a queryset's elements are returned, +you can use the ``reverse()`` method. Calling ``reverse()`` a second time +restores the ordering back to the normal direction. + +To retrieve the ''last'' five items in a queryset, you could do this:: + + my_queryset.reverse()[:5] + +Note that this is not quite the same as slicing from the end of a sequence in +Python. The above example will return the last item first, then the +penultimate item and so on. If we had a Python sequence and looked at +``seq[:-5]``, we would see the fifth-last item first. Django doesn't support +that mode of access (slicing from the end), since it is not possible to do it +efficiently in SQL. + ``distinct()`` ~~~~~~~~~~~~~~ @@ -531,10 +563,28 @@ eliminates duplicate rows from the query results. By default, a ``QuerySet`` will not eliminate duplicate rows. In practice, this is rarely a problem, because simple queries such as ``Blog.objects.all()`` -don't introduce the possibility of duplicate result rows. +don't introduce the possibility of duplicate result rows. However, if your +query spans multiple tables, it's possible to get duplicate results when a +``QuerySet`` is evaluated. That's when you'd use ``distinct()``. -However, if your query spans multiple tables, it's possible to get duplicate -results when a ``QuerySet`` is evaluated. That's when you'd use ``distinct()``. +.. note:: + Any fields used in an ``order_by()`` call are included in the SQL + ``SELECT`` columns. This can sometimes lead to unexpected results when + used in conjunction with ``distinct()``. If you order by fields from a + related model, those fields will be added to the selected columns and they + may make otherwise duplicate rows appear to be distinct. Since the extra + columns don't appear in the returned results (they are only there to + support ordering), it sometimes looks like non-distinct results are being + returned. + + Similarly, if you use a ``values()`` query to restrict the columns + selected, the columns used in any ``order_by()`` (or default model + ordering) will still be involved and may affect uniqueness of the results. + + The moral here is that if you are using ``distinct()`` be careful about + ordering by related models. Similarly, when using ``distinct()`` and + ``values()`` together, be careful when ordering by fields not in the + ``values()`` call. ``values(*fields)`` ~~~~~~~~~~~~~~~~~~~ @@ -569,6 +619,43 @@ Example:: >>> Blog.objects.values('id', 'name') [{'id': 1, 'name': 'Beatles Blog'}] +You can also retrieve values from across ``ForeignKey`` relations by using +double underscores to separate the field names, just as when calling the +``filter()`` command. For example:: + + >>> Entry.objects.values('blog__name').distinct() + [{'name': 'Beatles Blog'}] + +A couple of subtleties that are worth mentioning: + + * The ``values()`` method does not return anything for ``ManyToManyField`` + attributes and will raise an error if you try to pass in this type of + field to it. + * If you have a field called ``foo`` that is a ``ForeignKey``, the default + ``values()`` call will return a dictionary key called ``foo_id``, since + this is the name of the hidden model attribute that stores the actual + value (the ``foo`` attribute refers to the related model). When you are + calling ``values()`` and passing in field names, you can pass in either + ``foo`` or ``foo_id`` and you will get back the same thing (the + dictionary key will match the field name you passed in). + + For example:: + + >>> Entry.objects.values() + [{'blog_id: 1, 'headline': u'First Entry', ...}, ...] + + >>> Entry.objects.values('blog') + [{'blog': 1}, ...] + + >>> Entry.objects.values('blog_id') + [{'blog_id': 1}, ...] + * When using ``values()`` together with ``distinct()``, be aware that + ordering can affect the results. See the note in the `distinct()`_ + section, above, for details. + +**New in Django development version:** Previously, it was not possible to pass +``blog_id`` to ``values()`` in the above example, only ``blog``. + A ``ValuesQuerySet`` is useful when you know you're only going to need values from a small number of the available fields and you won't need the functionality of a model instance object. It's more efficient to select only @@ -586,6 +673,34 @@ followed (optionally) by any output-affecting methods (such as ``values()``), but it doesn't really matter. This is your chance to really flaunt your individualism. +``values_list(*fields)`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +This is similar to ``values()`` except that instead of returning a list of +dictionaries, it returns a list of tuples. Each tuple contains the value from +the respective field passed into the ``values_list()`` call -- so the first +item is the first field, etc. For example:: + + >>> Entry.objects.values_list('id', 'headline') + [(1, u'First entry'), ...] + +If you only pass in a single field, you can also pass in the ``flat`` +parameter. If ``True``, this will mean the returned results are single values, +rather than one-tuples. An example should make the difference clearer:: + + >>> Entry.objects.values_list('id').order_by('id') + [(1,), (2,), (3,), ...] + + >>> Entry.objects.values_list('id', flat=True).order_by('id') + [1, 2, 3, ...] + +It is an error to pass in ``flat`` when there is more than one field. + +If you don't pass any values to ``values_list()``, it will return all the +fields in the model, in the order they were declared. + ``dates(field, kind, order='ASC')`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -635,6 +750,17 @@ Examples:: >>> Entry.objects.none() [] +``all()`` +~~~~~~~~~~ + +**New in Django development version** + +Returns a ''copy'' of the current ``QuerySet`` (or ``QuerySet`` subclass you +pass in). This can be useful in some situations where you might want to pass +in either a model manager or a ``QuerySet`` and do further filtering on the +result. You can safely call ``all()`` on either object and then you'll +definitely have a ``QuerySet`` to work with. + ``select_related()`` ~~~~~~~~~~~~~~~~~~~~ @@ -687,8 +813,8 @@ related ``Person`` *and* the related ``City``:: p = b.author # Hits the database. c = p.hometown # Hits the database. -Note that ``select_related()`` does not follow foreign keys that have -``null=True``. +Note that, by default, ``select_related()`` does not follow foreign keys that +have ``null=True``. Usually, using ``select_related()`` can vastly improve performance because your app can avoid many database calls. However, in situations with deeply nested @@ -705,8 +831,43 @@ follow:: The ``depth`` argument is new in the Django development version. -``extra(select=None, where=None, params=None, tables=None)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**New in Django development version:** Sometimes you only need to access +specific models that are related to your root model, not all of the related +models. In these cases, you can pass the related field names to +``select_related()`` and it will only follow those relations. You can even do +this for models that are more than one relation away by separating the field +names with double underscores, just as for filters. For example, if we have +this model:: + + class Room(models.Model): + # ... + building = models.ForeignKey(...) + + class Group(models.Model): + # ... + teacher = models.ForeignKey(...) + room = models.ForeignKey(Room) + subject = models.ForeignKey(...) + +...and we only needed to work with the ``room`` and ``subject`` attributes, we +could write this:: + + g = Group.objects.select_related('room', 'subject') + +This is also valid:: + + g = Group.objects.select_related('room__building', 'subject') + +...and would also pull in the ``building`` relation. + +You can only refer to ``ForeignKey`` relations in the list of fields passed to +``select_related``. You *can* refer to foreign keys that have ``null=True`` +(unlike the default ``select_related()`` call). It's an error to use both a +list of fields and the ``depth`` parameter in the same ``select_related()`` +call, since they are conflicting options. + +``extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sometimes, the Django query syntax by itself can't easily express a complex ``WHERE`` clause. For these edge cases, Django provides the ``extra()`` @@ -763,6 +924,21 @@ of the arguments is required, but you should use at least one of them. some database backends, such as some MySQL versions, don't support subqueries. + **New in Django development version** + In some rare cases, you might wish to pass parameters to the SQL fragments + in ``extra(select=...)```. For this purpose, use the ``select_params`` + parameter. Since ``select_params`` is a sequence and the ``select`` + attribute is a dictionary, some care is required so that the parameters + are matched up correctly with the extra select pieces. In this situation, + you should use a ``django.utils.datastructures.SortedDict`` for the + ``select`` value, not just a normal Python dictionary. + + This will work, for example:: + + Blog.objects.extra( + select=SortedDict(('a', '%s'), ('b', '%s')), + select_params=('one', 'two')) + ``where`` / ``tables`` You can define explicit SQL ``WHERE`` clauses -- perhaps to perform non-explicit joins -- by using ``where``. You can manually add tables to @@ -779,20 +955,61 @@ of the arguments is required, but you should use at least one of them. SELECT * FROM blog_entry WHERE id IN (3, 4, 5, 20); + Be careful when using the ``tables`` parameter if you're specifying + tables that are already used in the query. When you add extra tables + via the ``tables`` parameter, Django assumes you want that table included + an extra time, if it is already included. That creates a problem, + since the table name will then be given an alias. If a table appears + multiple times in an SQL statement, the second and subsequent occurrences + must use aliases so the database can tell them apart. If you're + referring to the extra table you added in the extra ``where`` parameter + this is going to cause errors. + + Normally you'll only be adding extra tables that don't already appear in + the query. However, if the case outlined above does occur, there are a few + solutions. First, see if you can get by without including the extra table + and use the one already in the query. If that isn't possible, put your + ``extra()`` call at the front of the queryset construction so that your + table is the first use of that table. Finally, if all else fails, look at + the query produced and rewrite your ``where`` addition to use the alias + given to your extra table. The alias will be the same each time you + construct the queryset in the same way, so you can rely upon the alias + name to not change. + +``order_by`` + If you need to order the resulting queryset using some of the new fields + or tables you have included via ``extra()`` use the ``order_by`` parameter + to ``extra()`` and pass in a sequence of strings. These strings should + either be model fields (as in the normal ``order_by()`` method on + querysets), of the form ``table_name.column_name`` or an alias for a column + that you specified in the ``select`` parameter to ``extra()``. + + For example:: + + q = Entry.objects.extra(select={'is_recent': "pub_date > '2006-01-01'"}) + q = q.extra(order_by = ['-is_recent']) + + This would sort all the items for which ``is_recent`` is true to the front + of the result set (``True`` sorts before ``False`` in a descending + ordering). + + This shows, by the way, that you can make multiple calls to + ``extra()`` and it will behave as you expect (adding new constraints each + time). + ``params`` - The ``select`` and ``where`` parameters described above may use standard - Python database string placeholders -- ``'%s'`` to indicate parameters the - database engine should automatically quote. The ``params`` argument is a - list of any extra parameters to be substituted. + The ``where`` parameter described above may use standard Python database + string placeholders -- ``'%s'`` to indicate parameters the database engine + should automatically quote. The ``params`` argument is a list of any extra + parameters to be substituted. Example:: Entry.objects.extra(where=['headline=%s'], params=['Lennon']) - Always use ``params`` instead of embedding values directly into ``select`` - or ``where`` because ``params`` will ensure values are quoted correctly - according to your particular backend. (For example, quotes will be escaped - correctly.) + Always use ``params`` instead of embedding values directly into ``where`` + because ``params`` will ensure values are quoted correctly according to + your particular backend. (For example, quotes will be escaped correctly.) Bad:: @@ -802,8 +1019,9 @@ of the arguments is required, but you should use at least one of them. Entry.objects.extra(where=['headline=%s'], params=['Lennon']) - The combined number of placeholders in the list of strings for ``select`` - or ``where`` should equal the number of values in the ``params`` list. +**New in Django development version** The ``select_params`` argument to +``extra()`` is new. Previously, you could attempt to pass parameters for +``select`` in the ``params`` argument, but it worked very unreliably. QuerySet methods that do not return QuerySets --------------------------------------------- @@ -1031,7 +1249,12 @@ Examples:: SQL equivalents:: SELECT ... WHERE id = 14; - SELECT ... WHERE id = NULL; + SELECT ... WHERE id IS NULL; + +**New in Django development version:** The semantics of ``id__exact=None`` have +changed in the development version. Previously, it was (intentionally) +converted to ``WHERE id = NULL`` at the SQL level, which would never match +anything. It has now been changed to behave the same as ``id__isnull=True``. iexact ~~~~~~ @@ -1261,14 +1484,6 @@ SQL equivalent:: SELECT ... WHERE pub_date IS NULL; -.. admonition:: ``__isnull=True`` vs ``__exact=None`` - - There is an important difference between ``__isnull=True`` and - ``__exact=None``. ``__exact=None`` will *always* return an empty result - set, because SQL requires that no value is equal to ``NULL``. - ``__isnull`` determines if the field is currently holding the value - of ``NULL`` without performing a comparison. - search ~~~~~~ @@ -1368,6 +1583,11 @@ equivalent:: Entry.objects.filter(blog__id=3) # __exact is implied Entry.objects.filter(blog__pk=3) # __pk implies __id__exact +.. note:: + Because of this shortcut, you cannot have a field called ``pk`` that is not + the primary key of the model. It will always be replaced by the name of the + model's primary key in queries. + Lookups that span relationships ------------------------------- @@ -1392,6 +1612,60 @@ whose ``headline`` contains ``'Lennon'``:: Blog.objects.filter(entry__headline__contains='Lennon') +Spanning multi-valued relationships +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +When you are filtering an object based on a ``ManyToManyField`` or a reverse +``ForeignKeyField``, there are two different sorts of filter you may be +interested in. Consider the ``Blog``/``Entry`` relationship (``Blog`` to +``Entry`` is a one-to-many relation). We might be interested in finding blogs +that have an entry which has both *"Lennon"* in the headline and was published +today. Or we might want to find blogs that have an entry with *"Lennon"* in +the headline as well as an entry that was published today. Since there are +multiple entries associated with a single ``Blog``, both of these queries are +possible and make sense in some situations. + +The same type of situation arises with a ``ManyToManyField``. For example, if +an ``Entry`` has a ``ManyToManyField`` called ``tags``, we might want to find +entries linked to tags called *"music"* and *"bands"* or we might want an +entry that contains a tag with a name of *"music"* and a status of *"public"*. + +To handle both of these situations, Django has a consistent way of processing +``filter()`` and ``exclude()`` calls. Everything inside a single ``filter()`` +call is applied simultaneously to filter out items matching all those +requirements. Successive ``filter()`` calls further restrict the set of +objects, but for multi-valued relations, they apply to any object linked to +the primary model, not necessarily those objects that were selected by an +earlier ``filter()`` call. + +That may sound a bit confusing, so hopefully an example will clarify. To +select all blogs that contains entries with *"Lennon"* in the headline and +were published today, we would write:: + + Blog.objects.filter(entry__headline__contains='Lennon', + entry__pub_date=datetime.date.today()) + +To select all blogs that contain an entry with *"Lennon"* in the headline +**as well as** an entry that was published today, we would write:: + + Blog.objects.filter(entry__headline__contains='Lennon').filter( + entry__pub_date=datetime.date.today()) + +In this second example, the first filter restricted the queryset to all those +blogs linked to that particular type of entry. The second filter restricted +the set of blogs *further* to those that are also linked to the second type of +entry. The entries select by the second filter may or may not be the same as +the entries in the first filter. We are filtering the ``Blog`` items with each +filter statement, not the ``Entry`` items. + +All of this behaviour also applies to ``exclude()``: all the conditions in a +single ``exclude()`` statement apply to a single instance (if those conditions +are talking about the same multi-valued relation). Conditions in subsequent +``filter()`` or ``exclude()`` calls that refer to the same relation may end up +filtering on different linked objects. + Escaping percent signs and underscores in LIKE statements --------------------------------------------------------- @@ -1496,6 +1770,12 @@ This is equivalent to the following SQL ``WHERE`` clause:: You can compose statements of arbitrary complexity by combining ``Q`` objects with the ``&`` and ``|`` operators. You can also use parenthetical grouping. +**New in Django development version:** ``Q`` objects can also be negated using +the ``~`` operator, allowing for combined lookups that combine both a normal +query and a negated (``NOT``) query:: + + Q(question__startswith='Who') | ~Q(pub_date__year=2005) + Each lookup function that takes keyword-arguments (e.g. ``filter()``, ``exclude()``, ``get()``) can also be passed one or more ``Q`` objects as positional (not-named) arguments. If you provide multiple ``Q`` object @@ -1815,6 +2095,34 @@ complete query set:: Entry.objects.all().delete() +Updating multiple objects at once +================================= + +**New in Django development version** + +Sometimes you want to set a field to a particular value for all the objects in +a queryset. You can do this with the ``update()`` method. For example:: + + # Update all the headlines to the same value. + Entry.objects.all().update(headline='Everything is the same') + +You can only set non-relation fields and ``ForeignKey`` fields using this +method and the value you set the field to must be a normal Python value (you +can't set a field to be equal to some other field at the moment). + +To update ``ForeignKey`` fields, set the new value to be the new model +instance you want to point to. Example:: + + b = Blog.objects.get(pk=1) + # Make all entries belong to this blog. + Entry.objects.all().update(blog=b) + +The ``update()`` method is applied instantly and doesn't return anything +(similar to ``delete()``). The only restriction on the queryset that is +updated is that it can only access one database table, the model's main +table. So don't try to filter based on related fields or anything like that; +it won't work. + Extra instance methods ====================== diff --git a/docs/model-api.txt b/docs/model-api.txt index f73c5aadf7..4ed4ede97a 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -52,7 +52,7 @@ Some technical notes: * The name of the table, ``myapp_person``, is automatically derived from some model metadata but can be overridden. See `Table names`_ below. * An ``id`` field is added automatically, but this behavior can be - overriden. See `Automatic primary key fields`_ below. + overridden. See `Automatic primary key fields`_ below. * The ``CREATE TABLE`` SQL in this example is formatted using PostgreSQL syntax, but it's worth noting Django uses SQL tailored to the database backend specified in your `settings file`_. @@ -886,6 +886,10 @@ relationship should work. All are optional: `related objects documentation`_ for a full explanation and example. + If using this in an `abstract base class`_, be + sure to read the `extra notes`_ in that section + about ``related_name``. + ``to_field`` The field on the related object that the relation is to. By default, Django uses the primary key of the related object. @@ -893,6 +897,8 @@ relationship should work. All are optional: .. _`Database API reference`: ../db-api/ .. _related objects documentation: ../db-api/#related-objects +.. _abstract base class: `Abstract base classes`_ +.. _extra notes: `Be careful with related_name`_ Many-to-many relationships ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -988,9 +994,6 @@ the relationship should work. All are optional: One-to-one relationships ~~~~~~~~~~~~~~~~~~~~~~~~ -The semantics of one-to-one relationships will be changing soon, so we don't -recommend you use them. If that doesn't scare you away, keep reading. - To define a one-to-one relationship, use ``OneToOneField``. You use it just like any other ``Field`` type: by including it as a class attribute of your model. @@ -1012,9 +1015,26 @@ As with ``ForeignKey``, a relationship to self can be defined by using the string ``"self"`` instead of the model name; references to as-yet undefined models can be made by using a string containing the model name. -This ``OneToOneField`` will actually replace the primary key ``id`` field -(since one-to-one relations share the same primary key), and will be displayed -as a read-only field when you edit an object in the admin interface: +Finally, ``OneToOneField`` takes the following extra option: + + ======================= ============================================================ + Argument Description + ======================= ============================================================ + ``parent_link`` When ``True`` and used in a model inherited from + another model, indicates that this field should + be used as the link from the child back to the + parent. See `Model inheritance`_ for more + details. + + **New in Django development version** + + ======================= ============================================================ + +**New in Django development version:** ``OneToOneField`` classes used to +automatically become the primary key on a model. This is no longer true, +although you can manually pass in the ``primary_key`` attribute if you like. +Thus, it's now possible to have multiple fields of type ``OneToOneField`` on a +single model. See the `One-to-one relationship model example`_ for a full example. @@ -1048,6 +1068,14 @@ Model metadata is "anything that's not a field", such as ordering options, etc. Here's a list of all possible ``Meta`` options. No options are required. Adding ``class Meta`` to a model is completely optional. +``abstract`` +------------ + +**New in Django development version** + +When set to ``True``, denotes this model as an abstract base class. See +`Abstract base classes`_ for more details. Defaults to ``False``. + ``db_table`` ------------ @@ -1155,6 +1183,10 @@ together. It's used in the Django admin and is enforced at the database level (i.e., the appropriate ``UNIQUE`` statements are included in the ``CREATE TABLE`` statement). +All the fields specified in ``unique_together`` must be part of the current +model. If you are using `model inheritance`_, you cannot refer to fields from +any parent classes in ``unique_together``. + **New in Django development version** For convenience, unique_together can be a single list when dealing @@ -2041,6 +2073,238 @@ You can also prevent saving:: .. _database API docs: ../db-api/ +Model inheritance +================= + +**New in Django development version** + +Model inheritance in Django works almost identically to the way normal class +inheritance works in Python. The only decision you have to make is whether you +want the parent models to be models in their own right (with their own +database tables), or if the parents are just holders of common information +that will only be visible through the child models. + +Often, you will just want to use the parent class to hold information that you +don't want to have to type out for each child model. This class isn't going to +ever be used in isolation, so `abstract base classes`_ are what you're after. However, if you're subclassing an existing model (perhaps something from another application entirely), or want each model to have its own database table, `multi-table inheritance`_ is the way to go. + +Abstract base classes +--------------------- + +Abstract base classes are useful when you want to put some common information +into a number of other models. You write your base class and put +``abstract=True`` in the ``Meta`` class. This model will then not be used to +create any database table. Instead, when it is used as a base class for other +models, its fields will be added to those of the child class. It is an error +to have fields in the abstract base class with the same name as those in the +child (and Django will raise an exception). + +An example:: + + class CommonInfo(models.Model): + name = models.CharField(max_length=100) + age = models.PositiveIntegerField() + + class Meta: + abstract = True + + class Student(CommonInfo): + home_group = models.CharField(max_length=5) + +The ``Student`` model will have three fields: ``name``, ``age`` and +``home_group``. The ``CommonInfo`` model cannot be used as a normal Django +model, since it is an abstract base class. It does not generate a database +table or have a manager or anything like that. + +For many uses, this type of model inheritance will be exactly what you want. +It provides a way to factor out common information at the Python level, whilst +still only creating one database table per child model at the database level. + +``Meta`` inheritance +~~~~~~~~~~~~~~~~~~~~ + +When an abstract base class is created, Django makes any ``Meta`` inner class +you declared on the base class available as an attribute. If a child class +does not declared its own ``Meta`` class, it will inherit the parent's +``Meta``. If the child wants to extend the parent's ``Meta`` class, it can +subclass it. For example:: + + class CommonInfo(models.Model): + ... + class Meta: + abstract = True + ordering = ['name'] + + class Student(CommonInfo): + ... + class Meta(CommonInfo.Meta): + db_table = 'student_info' + +Django does make one adjustment to the ``Meta`` class of an abstract base +class: before installing the ``Meta`` attribute, it sets ``abstract=False``. +This means that children of abstract base classes don't automatically become +abstract classes themselves. Of course, you can make an abstract base class +that inherits from another abstract base class. You just need to remember to +explicitly set ``abstract=True`` each time. + +Some attributes won't make sense to include in the ``Meta`` class of an +abstract base class. For example, including ``db_table`` would mean that all +the child classes (the ones that don't specify their own ``Meta``) would use +the same database table, which is almost certainly not what you want. + +Be careful with ``related_name`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are using the ``related_name`` attribute on a ``ForeignKey`` or +``ManyToManyField``, you must always specify a *unique* reverse name for the +field. This would normally cause a problem in abstract base classes, since the +fields on this class are included into each of the child classes, with exactly +the same values for the attributes (including ``related_name``) each time. + +To work around this problem, when you are using ``related_name`` in an +abstract base class (only), part of the name should be the string +``'%(class)s'``. This is replaced by the lower-cased name of the child class +that the field is used in. Since each class has a different name, each related +name will end up being different. For example:: + + class Base(models.Model): + m2m = models.ManyToMany(OtherModel, related_name="%(class)s_related") + + class Meta: + abstract = True + + class ChildA(Base): + pass + + class ChildB(Base): + pass + +The reverse name of the ``ChildA.m2m`` field will be ``childa_related``, +whilst the reverse name of the ``ChildB.m2m`` field will be +``childb_related``. It is up to you how you use the ``'%(class)s'`` portion to +construct your related name, but if you forget to use it, Django will raise +errors when you validate your models (or run ``syncdb``). + +If you don't specify a ``related_name`` attribute for a field in an abstract +base class, the default reverse name will be the name of the child class +followed by ``'_set'``, just as it normally would be if you'd declared the field directly on the child class. For example, in the above code, if the ``related_name`` attribute was omitted, the reverse name for the ``m2m`` field would be ``childa_set`` in the ``ChildA`` case and ``childb_set`` for the ``ChildB`` field. + +Multi-table inheritance +----------------------- + +The second type of model inheritance supported by Django is when each model in +the hierarchy is a model all by itself. Each model corresponds to its own +database table and can be queried and created individually. The inheritance +relationship introduces links between the child model and each of its parents +(via an automatically created ``OneToOneField``). For example:: + + class Place(models.Model): + name = models.CharField(max_length=50) + address = models.CharField(max_length=80) + + class Restaurant(Place): + serves_hot_dogs = models.BooleanField() + serves_pizza = models.BooleanField() + +All of the fields of ``Place`` will also be available in ``Restaurant``, +although the data will reside in a different database table. So these are both +possible:: + + >>> Place.objects.filter(name="Bob's Cafe") + >>> Restaurant.objects.filter(name="Bob's Cafe") + +If you have a ``Place`` that is also a ``Restaurant``, you can get from the +``Place`` object to the ``Restaurant`` object by using the lower-case version +of the model name:: + + >>> p = Place.objects.filter(name="Bob's Cafe") + # If Bob's Cafe is a Restaurant object, this will give the child class: + >>> p.restaurant + + +However, if ``p`` in the above example was *not* a ``Restaurant`` (it had been +created directly as a ``Place`` object or was the parent of some other class), +referring to ``p.restaurant`` would give an error. + +``Meta`` and multi-table inheritance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the multi-table inheritance situation, it doesn't make sense for a child +class to inherit from its parent's ``Meta`` class. All the ``Meta`` options +have already been applied to the parent class and applying them again would +normally only lead to contradictory behaviour (this is in contrast with the +abstract base class case, where the base class doesn't exist in its own +right). + +So a child model does not have access to its parent's ``Meta`` class. However, +there are a few limited cases where the child inherits behaviour from the +parent: if the child does not specify an ``ordering`` attribute or a +``get_latest_by`` attribute, it will inherit these from its parent. + +If the parent has an ordering and you don't want the child to have any natural +ordering, you can explicitly set it to be empty:: + + class ChildModel(ParentModel): + ... + class Meta: + # Remove parent's ordering effect + ordering = [] + +Inheritance and reverse relations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Because multi-table inheritance uses an implicit ``OneToOneField`` to link the +child and the parent, it's possible to move from the parent down to the child, +as in the above example. However, this uses up the name that is the default +``related_name`` value for ``ForeignKey`` and ``ManyToManyField`` relations. +If you are putting those type of relations on a subclass of another model, you +**must** specify the ``related_name`` attribute on each such field. If you +forget, Django will raise an error when you run ``manage.py validate`` or try +to syncdb. + +For example, using the above ``Place`` class again, let's create another +subclass with a ``ManyToManyField``:: + + class Supplier(Place): + # Must specify related_name on all relations. + customers = models.ManyToManyField(Restaurant, + related_name='provider') + +For more information about reverse relations, refer to the `Database API +reference`_ . For now, just remember to run ``manage.py validate`` when +you're writing your models and pay attention to the error messages. + +Specifying the parent link field +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As mentioned, Django will automatically create a ``OneToOneField`` linking +your child class back any non-abstract parent models. If you want to control +the name of the attribute linking back to the parent, you can create your own +link field and pass it ``parent_link=True``. For example, to explicitly +specify the field that will link ``Supplier`` to ``Place`` in the above +example, you could write:: + + class Supplier(Place): + parent = models.OneToOneField(Place, parent_link=True) + ... + +Multiple inheritance +-------------------- + +Just as with Python's subclassing, it's possible for a Django model to inherit +from multiple parent models. Keep in mind that normal Python name resolution +rules apply. The first base class that a particular name appears in (e.g. +``Meta``) will be the one that is used. We stop searching once we find the +name once. This means that if multiple parents contain a ``Meta`` class, only +the first one is going to be used. All others will be ignored. + +Generally, you won't need to inherit from multiple parents. The main use-case +where this is useful is for ''mix-in'' classes: adding a particular extra +field or method to every class that inherits the mix-in. Try to keep your +inheritance hierarchies as simple and straightforward as possible so that you +won't have to struggle to work out where a particular piece of information is +coming from. + Models across files =================== diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py index 557331a36e..51de8a50f8 100644 --- a/tests/modeltests/basic/models.py +++ b/tests/modeltests/basic/models.py @@ -292,11 +292,9 @@ datetime.datetime(2005, 7, 28, 0, 0) >>> Article.objects.all()[2:][2:3] [] -# Note that you can't use 'offset' without 'limit' (on some dbs), so this doesn't work: ->>> Article.objects.all()[2:] -Traceback (most recent call last): - ... -AssertionError: 'offset' is not allowed without 'limit' +# Using an offset without a limit is also possible. +>>> Article.objects.all()[5:] +[, , ] # Also, once you have sliced you can't filter, re-order or combine >>> Article.objects.all()[0:5].filter(id=1) diff --git a/tests/modeltests/custom_columns/models.py b/tests/modeltests/custom_columns/models.py index e1d0bc6e94..a0800299a7 100644 --- a/tests/modeltests/custom_columns/models.py +++ b/tests/modeltests/custom_columns/models.py @@ -55,8 +55,8 @@ __test__ = {'API_TESTS':""" >>> art.save() >>> art.authors = [a, a2] -# Although the table and column names on Author have been set to -# custom values, nothing about using the Author model has changed... +# Although the table and column names on Author have been set to custom values, +# nothing about using the Author model has changed... # Query the available authors >>> Author.objects.all() @@ -71,7 +71,7 @@ __test__ = {'API_TESTS':""" >>> Author.objects.filter(firstname__exact='John') Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'firstname' into field. Choices are: article, id, first_name, last_name +FieldError: Cannot resolve keyword 'firstname' into field. Choices are: article, first_name, id, last_name >>> a = Author.objects.get(last_name__exact='Smith') >>> a.first_name diff --git a/tests/modeltests/field_subclassing/models.py b/tests/modeltests/field_subclassing/models.py index 97804f5cd5..baf07a072f 100644 --- a/tests/modeltests/field_subclassing/models.py +++ b/tests/modeltests/field_subclassing/models.py @@ -5,6 +5,7 @@ Tests for field subclassing. from django.db import models from django.utils.encoding import force_unicode from django.core import serializers +from django.core.exceptions import FieldError class Small(object): """ @@ -50,7 +51,7 @@ class SmallField(models.Field): return [force_unicode(v) for v in value] if lookup_type == 'isnull': return [] - raise TypeError('Invalid lookup type: %r' % lookup_type) + raise FieldError('Invalid lookup type: %r' % lookup_type) def flatten_data(self, follow, obj=None): return {self.attname: force_unicode(self._get_val_from_obj(obj))} @@ -94,7 +95,7 @@ True >>> MyModel.objects.filter(data__lt=s) Traceback (most recent call last): ... -TypeError: Invalid lookup type: 'lt' +FieldError: Invalid lookup type: 'lt' # Serialization works, too. >>> stream = serializers.serialize("json", MyModel.objects.all()) diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index f31857e0fe..7a53e93aec 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -162,12 +162,36 @@ True >>> Article.objects.extra(select={'id_plus_one': 'id + 1'}).values('id', 'id_plus_two') Traceback (most recent call last): ... -FieldDoesNotExist: Article has no field named 'id_plus_two' +FieldError: Cannot resolve keyword 'id_plus_two' into field. Choices are: headline, id, id_plus_one, pub_date # If you don't specify field names to values(), all are returned. >>> list(Article.objects.filter(id=5).values()) == [{'id': 5, 'headline': 'Article 5', 'pub_date': datetime(2005, 8, 1, 9, 0)}] True +# values_list() is similar to values(), except that the results are returned as +# a list of tuples, rather than a list of dictionaries. Within each tuple, the +# order of the elemnts is the same as the order of fields in the values_list() +# call. +>>> Article.objects.values_list('headline') +[(u'Article 5',), (u'Article 6',), (u'Article 4',), (u'Article 2',), (u'Article 3',), (u'Article 7',), (u'Article 1',)] + +>>> Article.objects.values_list('id').order_by('id') +[(1,), (2,), (3,), (4,), (5,), (6,), (7,)] +>>> Article.objects.values_list('id', flat=True).order_by('id') +[1, 2, 3, 4, 5, 6, 7] + +>>> Article.objects.extra(select={'id_plus_one': 'id+1'}).order_by('id').values_list('id') +[(1,), (2,), (3,), (4,), (5,), (6,), (7,)] +>>> Article.objects.extra(select={'id_plus_one': 'id+1'}).order_by('id').values_list('id_plus_one', 'id') +[(2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7)] +>>> Article.objects.extra(select={'id_plus_one': 'id+1'}).order_by('id').values_list('id', 'id_plus_one') +[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)] + +>>> Article.objects.values_list('id', 'headline', flat=True) +Traceback (most recent call last): +... +TypeError: 'flat' is not valid when values_list is called with more than one field. + # Every DateField and DateTimeField creates get_next_by_FOO() and # get_previous_by_FOO() methods. # In the case of identical date values, these methods will use the ID as a @@ -240,6 +264,8 @@ DoesNotExist: Article matching query does not exist. [] >>> Article.objects.none().filter(headline__startswith='Article') [] +>>> Article.objects.filter(headline__startswith='Article').none() +[] >>> Article.objects.none().count() 0 >>> [article for article in Article.objects.none().iterator()] @@ -256,12 +282,12 @@ DoesNotExist: Article matching query does not exist. >>> Article.objects.filter(pub_date_year='2005').count() Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'pub_date_year' into field. Choices are: id, headline, pub_date +FieldError: Cannot resolve keyword 'pub_date_year' into field. Choices are: headline, id, pub_date >>> Article.objects.filter(headline__starts='Article') Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'headline__starts' into field. Choices are: id, headline, pub_date +FieldError: Join on field 'headline' not permitted. # Create some articles with a bit more interesting headlines for testing field lookups: >>> now = datetime.now() diff --git a/tests/modeltests/many_to_many/models.py b/tests/modeltests/many_to_many/models.py index 198c95c4d5..e09fd825f8 100644 --- a/tests/modeltests/many_to_many/models.py +++ b/tests/modeltests/many_to_many/models.py @@ -126,6 +126,11 @@ __test__ = {'API_TESTS':""" >>> Publication.objects.filter(article__in=[a1,a2]).distinct() [, , , ] +# Excluding a related item works as you would expect, too (although the SQL +# involved is a little complex). +>>> Article.objects.exclude(publications=p2) +[] + # If we delete a Publication, its Articles won't be able to access it. >>> p1.delete() >>> Publication.objects.all() diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py index d5d07a1e21..6616f8b55e 100644 --- a/tests/modeltests/many_to_one/models.py +++ b/tests/modeltests/many_to_one/models.py @@ -145,18 +145,18 @@ False [, ] # The underlying query only makes one join when a related table is referenced twice. ->>> query = Article.objects.filter(reporter__first_name__exact='John', reporter__last_name__exact='Smith') ->>> null, sql, null = query._get_sql_clause() +>>> queryset = Article.objects.filter(reporter__first_name__exact='John', reporter__last_name__exact='Smith') +>>> sql = queryset.query.as_sql()[0] >>> sql.count('INNER JOIN') 1 # The automatically joined table has a predictable name. ->>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_article__reporter.last_name='Smith'"]) +>>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_reporter.last_name='Smith'"]) [, ] # And should work fine with the unicode that comes out of # newforms.Form.cleaned_data ->>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_article__reporter.last_name='%s'" % u'Smith']) +>>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_reporter.last_name='%s'" % u'Smith']) [, ] # Find all Articles for the Reporter whose ID is 1. @@ -179,13 +179,13 @@ False >>> Article.objects.filter(reporter_id__exact=1) Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, headline, pub_date, reporter +FieldError: Cannot resolve keyword 'reporter_id' into field. Choices are: headline, id, pub_date, reporter # You need to specify a comparison clause >>> Article.objects.filter(reporter_id=1) Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, headline, pub_date, reporter +FieldError: Cannot resolve keyword 'reporter_id' into field. Choices are: headline, id, pub_date, reporter # You can also instantiate an Article by passing # the Reporter's ID instead of a Reporter object. @@ -250,6 +250,11 @@ TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, hea >>> Reporter.objects.filter(article__reporter=r).distinct() [] +# It's possible to use values() calls across many-to-one relations. (Note, too, that we clear the ordering here so as not to drag the 'headline' field into the columns being used to determine uniqueness.) +>>> d = {'reporter__first_name': u'John', 'reporter__last_name': u'Smith'} +>>> list(Article.objects.filter(reporter=r).distinct().order_by().values('reporter__first_name', 'reporter__last_name')) == [d] +True + # If you delete a reporter, his articles will be deleted. >>> Article.objects.all() [, , , , ] diff --git a/tests/modeltests/many_to_one_null/models.py b/tests/modeltests/many_to_one_null/models.py index 60c5888371..cee0e21a72 100644 --- a/tests/modeltests/many_to_one_null/models.py +++ b/tests/modeltests/many_to_one_null/models.py @@ -80,6 +80,11 @@ None >>> Article.objects.filter(reporter__isnull=True) [] +# We can achieve the same thing by filtering for the case where the reporter is +# None. +>>> Article.objects.filter(reporter=None) +[] + # Set the reporter for the Third article >>> r.article_set.add(a3) >>> r.article_set.all() diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index ca00e96418..b1a751f5e8 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -1,11 +1,53 @@ """ XX. Model inheritance -Model inheritance isn't yet supported. +Model inheritance exists in two varieties: + - abstract base classes which are a way of specifying common + information inherited by the subclasses. They don't exist as a separate + model. + - non-abstract base classes (the default), which are models in their own + right with their own database tables and everything. Their subclasses + have references back to them, created automatically. + +Both styles are demonstrated here. """ from django.db import models +# +# Abstract base classes +# + +class CommonInfo(models.Model): + name = models.CharField(max_length=50) + age = models.PositiveIntegerField() + + class Meta: + abstract = True + ordering = ['name'] + + def __unicode__(self): + return u'%s %s' % (self.__class__.__name__, self.name) + +class Worker(CommonInfo): + job = models.CharField(max_length=50) + +class Student(CommonInfo): + school_class = models.CharField(max_length=10) + + class Meta: + pass + +# +# Multi-table inheritance +# + +class Chef(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return u"%s the chef" % self.name + class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) @@ -13,9 +55,20 @@ class Place(models.Model): def __unicode__(self): return u"%s the place" % self.name -class Restaurant(Place): +class Rating(models.Model): + rating = models.IntegerField(null=True, blank=True) + + class Meta: + abstract = True + ordering = ['-rating'] + +class Restaurant(Place, Rating): serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() + chef = models.ForeignKey(Chef, null=True, blank=True) + + class Meta(Rating.Meta): + db_table = 'my_restaurant' def __unicode__(self): return u"%s the restaurant" % self.name @@ -26,14 +79,58 @@ class ItalianRestaurant(Restaurant): def __unicode__(self): return u"%s the italian restaurant" % self.name -__test__ = {'API_TESTS':""" -# Make sure Restaurant has the right fields in the right order. ->>> [f.name for f in Restaurant._meta.fields] -['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza'] +class Supplier(Place): + customers = models.ManyToManyField(Restaurant, related_name='provider') -# Make sure ItalianRestaurant has the right fields in the right order. ->>> [f.name for f in ItalianRestaurant._meta.fields] -['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza', 'serves_gnocchi'] + def __unicode__(self): + return u"%s the supplier" % self.name + +class ParkingLot(Place): + # An explicit link to the parent (we can control the attribute name). + parent = models.OneToOneField(Place, primary_key=True, parent_link=True) + main_site = models.ForeignKey(Place, related_name='lot') + + def __unicode__(self): + return u"%s the parking lot" % self.name + +__test__ = {'API_TESTS':""" +# The Student and Worker models both have 'name' and 'age' fields on them and +# inherit the __unicode__() method, just as with normal Python subclassing. +# This is useful if you want to factor out common information for programming +# purposes, but still completely independent separate models at the database +# level. + +>>> w = Worker(name='Fred', age=35, job='Quarry worker') +>>> w.save() +>>> w2 = Worker(name='Barney', age=34, job='Quarry worker') +>>> w2.save() +>>> s = Student(name='Pebbles', age=5, school_class='1B') +>>> s.save() +>>> unicode(w) +u'Worker Fred' +>>> unicode(s) +u'Student Pebbles' + +# The children inherit the Meta class of their parents (if they don't specify +# their own). +>>> Worker.objects.values('name') +[{'name': u'Barney'}, {'name': u'Fred'}] + +# Since Student does not subclass CommonInfo's Meta, it has the effect of +# completely overriding it. So ordering by name doesn't take place for Students. +>>> Student._meta.ordering +[] + +# However, the CommonInfo class cannot be used as a normal model (it doesn't +# exist as a model). +>>> CommonInfo.objects.all() +Traceback (most recent call last): + ... +AttributeError: type object 'CommonInfo' has no attribute 'objects' + +# The Place/Restaurant/ItalianRestaurant models, on the other hand, all exist +# as independent models. However, the subclasses also have transparent access +# to the fields of their ancestors. # Create a couple of Places. >>> p1 = Place(name='Master Shakes', address='666 W. Jersey') @@ -41,13 +138,131 @@ __test__ = {'API_TESTS':""" >>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland') >>> p2.save() -# Test constructor for Restaurant. ->>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False) +Test constructor for Restaurant. +>>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton',serves_hot_dogs=True, serves_pizza=False, rating=2) >>> r.save() # Test the constructor for ItalianRestaurant. ->>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Elm', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True) +>>> c = Chef(name="Albert") +>>> c.save() +>>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Ash', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True, rating=4, chef=c) +>>> ir.save() +>>> ir.address = '1234 W. Elm' >>> ir.save() +# Make sure Restaurant and ItalianRestaurant have the right fields in the right +# order. +>>> [f.name for f in Restaurant._meta.fields] +['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza', 'chef'] +>>> [f.name for f in ItalianRestaurant._meta.fields] +['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza', 'chef', 'restaurant_ptr', 'serves_gnocchi'] +>>> Restaurant._meta.ordering +['-rating'] + +# Even though p.supplier for a Place 'p' (a parent of a Supplier), a Restaurant +# object cannot access that reverse relation, since it's not part of the +# Place-Supplier Hierarchy. +>>> Place.objects.filter(supplier__name='foo') +[] +>>> Restaurant.objects.filter(supplier__name='foo') +Traceback (most recent call last): + ... +FieldError: Cannot resolve keyword 'supplier' into field. Choices are: address, chef, id, italianrestaurant, lot, name, place_ptr, provider, rating, serves_hot_dogs, serves_pizza + +# Parent fields can be used directly in filters on the child model. +>>> Restaurant.objects.filter(name='Demon Dogs') +[] +>>> ItalianRestaurant.objects.filter(address='1234 W. Elm') +[] + +# Filters against the parent model return objects of the parent's type. +>>> Place.objects.filter(name='Demon Dogs') +[] + +# Since the parent and child are linked by an automatically created +# OneToOneField, you can get from the parent to the child by using the child's +# name. +>>> place = Place.objects.get(name='Demon Dogs') +>>> place.restaurant + + +>>> Place.objects.get(name='Ristorante Miron').restaurant.italianrestaurant + +>>> Restaurant.objects.get(name='Ristorante Miron').italianrestaurant + + +# This won't work because the Demon Dogs restaurant is not an Italian +# restaurant. +>>> place.restaurant.italianrestaurant +Traceback (most recent call last): + ... +DoesNotExist: ItalianRestaurant matching query does not exist. + +# Related objects work just as they normally do. + +>>> s1 = Supplier(name="Joe's Chickens", address='123 Sesame St') +>>> s1.save() +>>> s1.customers = [r, ir] +>>> s2 = Supplier(name="Luigi's Pasta", address='456 Sesame St') +>>> s2.save() +>>> s2.customers = [ir] + +# This won't work because the Place we select is not a Restaurant (it's a +# Supplier). +>>> p = Place.objects.get(name="Joe's Chickens") +>>> p.restaurant +Traceback (most recent call last): + ... +DoesNotExist: Restaurant matching query does not exist. + +# But we can descend from p to the Supplier child, as expected. +>>> p.supplier + + +>>> ir.provider.order_by('-name') +[, ] + +>>> Restaurant.objects.filter(provider__name__contains="Chickens") +[, ] +>>> ItalianRestaurant.objects.filter(provider__name__contains="Chickens") +[] + +>>> park1 = ParkingLot(name='Main St', address='111 Main St', main_site=s1) +>>> park1.save() +>>> park2 = ParkingLot(name='Well Lit', address='124 Sesame St', main_site=ir) +>>> park2.save() + +>>> Restaurant.objects.get(lot__name='Well Lit') + + +# The update() command can update fields in parent and child classes at once +# (although it executed multiple SQL queries to do so). +>>> Restaurant.objects.filter(serves_hot_dogs=True, name__contains='D').update(name='Demon Puppies', serves_hot_dogs=False) +>>> r1 = Restaurant.objects.get(pk=r.pk) +>>> r1.serves_hot_dogs == False +True +>>> r1.name +u'Demon Puppies' + +# The values() command also works on fields from parent models. +>>> d = {'rating': 4, 'name': u'Ristorante Miron'} +>>> list(ItalianRestaurant.objects.values('name', 'rating')) == [d] +True + +# select_related works with fields from the parent object as if they were a +# normal part of the model. +>>> from django import db +>>> from django.conf import settings +>>> settings.DEBUG = True +>>> db.reset_queries() +>>> ItalianRestaurant.objects.all()[0].chef + +>>> len(db.connection.queries) +2 +>>> ItalianRestaurant.objects.select_related('chef')[0].chef + +>>> len(db.connection.queries) +3 +>>> settings.DEBUG = False """} diff --git a/tests/modeltests/one_to_one/models.py b/tests/modeltests/one_to_one/models.py index 2f3f22e628..800ccddac2 100644 --- a/tests/modeltests/one_to_one/models.py +++ b/tests/modeltests/one_to_one/models.py @@ -6,7 +6,7 @@ To define a one-to-one relationship, use ``OneToOneField()``. In this example, a ``Place`` optionally can be a ``Restaurant``. """ -from django.db import models +from django.db import models, connection class Place(models.Model): name = models.CharField(max_length=50) @@ -16,7 +16,7 @@ class Place(models.Model): return u"%s the place" % self.name class Restaurant(models.Model): - place = models.OneToOneField(Place) + place = models.OneToOneField(Place, primary_key=True) serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() @@ -38,6 +38,14 @@ class RelatedModel(models.Model): link = models.OneToOneField(ManualPrimaryKey) name = models.CharField(max_length = 50) +class MultiModel(models.Model): + link1 = models.OneToOneField(Place) + link2 = models.OneToOneField(ManualPrimaryKey) + name = models.CharField(max_length=50) + + def __unicode__(self): + return u"Multimodel %s" % self.name + __test__ = {'API_TESTS':""" # Create a couple of Places. >>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton') @@ -63,8 +71,8 @@ Traceback (most recent call last): ... DoesNotExist: Restaurant matching query does not exist. -# Set the place using assignment notation. Because place is the primary key on Restaurant, -# the save will create a new restaurant +# Set the place using assignment notation. Because place is the primary key on +# Restaurant, the save will create a new restaurant >>> r.place = p2 >>> r.save() >>> p2.restaurant @@ -72,9 +80,9 @@ DoesNotExist: Restaurant matching query does not exist. >>> r.place -# Set the place back again, using assignment in the reverse direction -# Need to reget restaurant object first, because the reverse set -# can't update the existing restaurant instance +# Set the place back again, using assignment in the reverse direction. Need to +# reload restaurant object first, because the reverse set can't update the +# existing restaurant instance >>> p1.restaurant = r >>> r.save() >>> p1.restaurant @@ -86,8 +94,7 @@ DoesNotExist: Restaurant matching query does not exist. # Restaurant.objects.all() just returns the Restaurants, not the Places. # Note that there are two restaurants - Ace Hardware the Restaurant was created -# in the call to r.place = p2. This means there are multiple restaurants referencing -# a single place... +# in the call to r.place = p2. >>> Restaurant.objects.all() [, ] @@ -165,4 +172,22 @@ DoesNotExist: Restaurant matching query does not exist. >>> o1.save() >>> o2 = RelatedModel(link=o1, name="secondary") >>> o2.save() + +# You can have multiple one-to-one fields on a model, too. +>>> x1 = MultiModel(link1=p1, link2=o1, name="x1") +>>> x1.save() +>>> o1.multimodel + + +# This will fail because each one-to-one field must be unique (and link2=o1 was +# used for x1, above). +>>> MultiModel(link1=p2, link2=o1, name="x1").save() +Traceback (most recent call last): + ... +IntegrityError: ... + +# Because the unittests all use a single connection, we need to force a +# reconnect here to ensure the connection is clean (after the previous +# IntegrityError). +>>> connection.close() """} diff --git a/tests/modeltests/or_lookups/models.py b/tests/modeltests/or_lookups/models.py index 44dda0dc16..c779e19e37 100644 --- a/tests/modeltests/or_lookups/models.py +++ b/tests/modeltests/or_lookups/models.py @@ -4,11 +4,9 @@ To perform an OR lookup, or a lookup that combines ANDs and ORs, combine QuerySet objects using & and | operators. -Alternatively, use positional arguments, and pass one or more expressions -of clauses using the variable ``django.db.models.Q`` (or any object with -a get_sql method). - - +Alternatively, use positional arguments, and pass one or more expressions of +clauses using the variable ``django.db.models.Q`` (or any object with an +add_to_query method). """ from django.db import models @@ -72,6 +70,8 @@ __test__ = {'API_TESTS':""" # You could also use "in" to accomplish the same as above. >>> Article.objects.filter(pk__in=[1,2,3]) [, , ] +>>> Article.objects.filter(pk__in=(1,2,3)) +[, , ] >>> Article.objects.filter(pk__in=[1,2,3,4]) [, , ] @@ -92,6 +92,17 @@ __test__ = {'API_TESTS':""" >>> Article.objects.filter(Q(headline__contains='bye'), headline__startswith='Hello') [] +# Q objects can be negated +>>> Article.objects.filter(Q(pk=1) | ~Q(pk=2)) +[, ] +>>> Article.objects.filter(~Q(pk=1) & ~Q(pk=2)) +[] + +# This allows for more complex queries than filter() and exclude() alone would +# allow +>>> Article.objects.filter(Q(pk=1) & (~Q(pk=2) | Q(pk=3))) +[] + # Try some arg queries with operations other than filter. >>> Article.objects.get(Q(headline__startswith='Hello'), Q(headline__contains='bye')) diff --git a/tests/modeltests/order_with_respect_to/__init__.py b/tests/modeltests/order_with_respect_to/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/order_with_respect_to/models.py b/tests/modeltests/order_with_respect_to/models.py new file mode 100644 index 0000000000..9c8917f59b --- /dev/null +++ b/tests/modeltests/order_with_respect_to/models.py @@ -0,0 +1,78 @@ +""" +Tests for the order_with_respect_to Meta attribute. +""" + +from django.db import models + +class Question(models.Model): + text = models.CharField(max_length=200) + +class Answer(models.Model): + text = models.CharField(max_length=200) + question = models.ForeignKey(Question) + + class Meta: + order_with_respect_to = 'question' + + def __unicode__(self): + return unicode(self.text) + +__test__ = {'API_TESTS': """ +>>> q1 = Question(text="Which Beatle starts with the letter 'R'?") +>>> q1.save() +>>> q2 = Question(text="What is your name?") +>>> q2.save() +>>> Answer(text="John", question=q1).save() +>>> Answer(text="Jonno",question=q2).save() +>>> Answer(text="Paul", question=q1).save() +>>> Answer(text="Paulo", question=q2).save() +>>> Answer(text="George", question=q1).save() +>>> Answer(text="Ringo", question=q1).save() + +The answers will always be ordered in the order they were inserted. + +>>> q1.answer_set.all() +[, , , ] + +We can retrieve the answers related to a particular object, in the order +they were created, once we have a particular object. + +>>> a1 = Answer.objects.filter(question=q1)[0] +>>> a1 + +>>> a2 = a1.get_next_in_order() +>>> a2 + +>>> a4 = list(Answer.objects.filter(question=q1))[-1] +>>> a4 + +>>> a4.get_previous_in_order() + + +Determining (and setting) the ordering for a particular item is also possible. + +>>> id_list = [o.pk for o in q1.answer_set.all()] +>>> a2.question.get_answer_order() == id_list +True + +>>> a5 = Answer(text="Number five", question=q1) +>>> a5.save() + +It doesn't matter which answer we use to check the order, it will always be the same. + +>>> a2.question.get_answer_order() == a5.question.get_answer_order() +True + +The ordering can be altered: + +>>> id_list = [o.pk for o in q1.answer_set.all()] +>>> x = id_list.pop() +>>> id_list.insert(-1, x) +>>> a5.question.get_answer_order == id_list +False +>>> a5.question.set_answer_order(id_list) +>>> q1.answer_set.all() +[, , , , ] + +""" +} diff --git a/tests/modeltests/ordering/models.py b/tests/modeltests/ordering/models.py index 9b342a9265..d7435f6f74 100644 --- a/tests/modeltests/ordering/models.py +++ b/tests/modeltests/ordering/models.py @@ -48,6 +48,13 @@ __test__ = {'API_TESTS':""" >>> Article.objects.order_by('pub_date', '-headline') [, , , ] +# Only the last order_by has any effect (since they each override any previous +# ordering). +>>> Article.objects.order_by('id') +[, , , ] +>>> Article.objects.order_by('id').order_by('-headline') +[, , , ] + # Use the 'stop' part of slicing notation to limit the results. >>> Article.objects.order_by('headline')[:2] [, ] @@ -64,4 +71,10 @@ __test__ = {'API_TESTS':""" # don't know what order the output will be in. >>> Article.objects.order_by('?') [...] + +# Ordering can be reversed using the reverse() method on a queryset. This +# allows you to extract things like "the last two items" (reverse and then +# take the first two). +>>> Article.objects.all().reverse()[:2] +[, ] """} diff --git a/tests/modeltests/reserved_names/models.py b/tests/modeltests/reserved_names/models.py index a11b8d9f88..f698b5bc49 100644 --- a/tests/modeltests/reserved_names/models.py +++ b/tests/modeltests/reserved_names/models.py @@ -45,8 +45,6 @@ h b >>> print v.where 2005-01-01 ->>> Thing.objects.order_by('select.when') -[, ] >>> Thing.objects.dates('where', 'year') [datetime.datetime(2005, 1, 1, 0, 0), datetime.datetime(2006, 1, 1, 0, 0)] diff --git a/tests/modeltests/reverse_lookup/models.py b/tests/modeltests/reverse_lookup/models.py index 80408ad761..ef385b4b18 100644 --- a/tests/modeltests/reverse_lookup/models.py +++ b/tests/modeltests/reverse_lookup/models.py @@ -55,5 +55,5 @@ __test__ = {'API_TESTS':""" >>> Poll.objects.get(choice__name__exact="This is the answer") Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'choice' into field. Choices are: poll_choice, related_choice, id, question, creator +FieldError: Cannot resolve keyword 'choice' into field. Choices are: creator, id, poll_choice, question, related_choice """} diff --git a/tests/modeltests/select_related/models.py b/tests/modeltests/select_related/models.py index f0fd121665..9d64cf24c6 100644 --- a/tests/modeltests/select_related/models.py +++ b/tests/modeltests/select_related/models.py @@ -27,13 +27,13 @@ class Phylum(models.Model): kingdom = models.ForeignKey(Kingdom) def __unicode__(self): return self.name - + class Klass(models.Model): name = models.CharField(max_length=50) phylum = models.ForeignKey(Phylum) def __unicode__(self): return self.name - + class Order(models.Model): name = models.CharField(max_length=50) klass = models.ForeignKey(Klass) @@ -63,7 +63,7 @@ def create_tree(stringtree): names = stringtree.split() models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species] assert len(names) == len(models), (names, models) - + parent = None for name, model in zip(names, models): try: @@ -100,7 +100,7 @@ __test__ = {'API_TESTS':""" # However, a select_related() call will fill in those related objects without any extra queries: >>> db.reset_queries() ->>> person = Species.objects.select_related().get(name="sapiens") +>>> person = Species.objects.select_related(depth=10).get(name="sapiens") >>> person.genus.family.order.klass.phylum.kingdom.domain >>> len(db.connection.queries) @@ -129,7 +129,7 @@ __test__ = {'API_TESTS':""" >>> pea.genus.family.order.klass.phylum.kingdom.domain -# Notice: one few query than above because of depth=1 +# Notice: one fewer queries than above because of depth=1 >>> len(db.connection.queries) 7 @@ -147,6 +147,43 @@ __test__ = {'API_TESTS':""" >>> len(db.connection.queries) 5 +>>> s = Species.objects.all().select_related(depth=1).extra(select={'a': 'select_related_species.id + 10'})[0] +>>> s.id + 10 == s.a +True + +# The optional fields passed to select_related() control which related models +# we pull in. This allows for smaller queries and can act as an alternative +# (or, in addition to) the depth parameter. + +# In the next two cases, we explicitly say to select the 'genus' and +# 'genus.family' models, leading to the same number of queries as before. +>>> db.reset_queries() +>>> world = Species.objects.select_related('genus__family') +>>> [o.genus.family for o in world] +[, , , ] +>>> len(db.connection.queries) +1 + +>>> db.reset_queries() +>>> world = Species.objects.filter(genus__name='Amanita').select_related('genus__family') +>>> [o.genus.family.order for o in world] +[] +>>> len(db.connection.queries) +2 + +>>> db.reset_queries() +>>> Species.objects.all().select_related('genus__family__order').order_by('id')[0:1].get().genus.family.order.name +u'Diptera' +>>> len(db.connection.queries) +1 + +# Specifying both "depth" and fields is an error. +>>> Species.objects.select_related('genus__family__order', depth=4) +Traceback (most recent call last): +... +TypeError: Cannot pass both "depth" and fields to select_related() + # Reset DEBUG to where we found it. >>> settings.DEBUG = False """} + diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py index 0ccc19f895..f9dd8f288b 100644 --- a/tests/modeltests/serializers/models.py +++ b/tests/modeltests/serializers/models.py @@ -22,7 +22,7 @@ class Author(models.Model): class Meta: ordering = ('name',) - + def __unicode__(self): return self.name @@ -39,21 +39,21 @@ class Article(models.Model): return self.headline class AuthorProfile(models.Model): - author = models.OneToOneField(Author) + author = models.OneToOneField(Author, primary_key=True) date_of_birth = models.DateField() - + def __unicode__(self): return u"Profile of %s" % self.author - + class Actor(models.Model): name = models.CharField(max_length=20, primary_key=True) class Meta: ordering = ('name',) - + def __unicode__(self): return self.name - + class Movie(models.Model): actor = models.ForeignKey(Actor) title = models.CharField(max_length=50) @@ -63,7 +63,7 @@ class Movie(models.Model): def __unicode__(self): return self.title - + class Score(models.Model): score = models.FloatField() @@ -100,7 +100,7 @@ __test__ = {'API_TESTS':""" >>> dom = minidom.parseString(xml) # Deserializing has a similar interface, except that special DeserializedObject -# instances are returned. This is because data might have changed in the +# instances are returned. This is because data might have changed in the # database since the data was serialized (we'll simulate that below). >>> for obj in serializers.deserialize("xml", xml): ... print obj @@ -148,7 +148,7 @@ __test__ = {'API_TESTS':""" >>> Article.objects.all() [, ] -# If you use your own primary key field (such as a OneToOneField), +# If you use your own primary key field (such as a OneToOneField), # it doesn't appear in the serialized field list - it replaces the # pk identifier. >>> profile = AuthorProfile(author=joe, date_of_birth=datetime(1970,1,1)) @@ -186,7 +186,7 @@ __test__ = {'API_TESTS':""" >>> print serializers.serialize("json", Article.objects.all(), fields=('headline','pub_date')) [{"pk": 1, "model": "serializers.article", "fields": {"headline": "Just kidding; I love TV poker", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 2, "model": "serializers.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:11"}}, {"pk": 3, "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00"}}] -# Every string is serialized as a unicode object, also primary key +# Every string is serialized as a unicode object, also primary key # which is 'varchar' >>> ac = Actor(name="Zażółć") >>> mv = Movie(title="Gęślą jaźń", actor=ac) @@ -247,12 +247,13 @@ try: pk: 2 ->>> obs = list(serializers.deserialize("yaml", serialized)) ->>> for i in obs: +>>> obs = list(serializers.deserialize("yaml", serialized)) +>>> for i in obs: ... print i """ -except ImportError: pass - +except ImportError: + pass + diff --git a/tests/modeltests/signals/models.py b/tests/modeltests/signals/models.py index 5b0f5f2285..fc58d90a14 100644 --- a/tests/modeltests/signals/models.py +++ b/tests/modeltests/signals/models.py @@ -66,7 +66,8 @@ post_save_nokwargs signal post_save signal, Tom Smith Is updated ->>> p1.save(raw=True) +# Calling an internal method purely so that we can trigger a "raw" save. +>>> p1.save_base(raw=True) pre_save_nokwargs signal pre_save signal, Tom Smith Is raw diff --git a/tests/modeltests/transactions/models.py b/tests/modeltests/transactions/models.py index 06d21bbdd4..a3222cd511 100644 --- a/tests/modeltests/transactions/models.py +++ b/tests/modeltests/transactions/models.py @@ -25,7 +25,7 @@ from django.conf import settings building_docs = getattr(settings, 'BUILDING_DOCS', False) -if building_docs or settings.DATABASE_ENGINE != 'mysql': +if building_docs or settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): __test__['API_TESTS'] += """ # the default behavior is to autocommit after each save() action >>> def create_a_reporter_then_fail(first, last): diff --git a/tests/modeltests/update/__init__.py b/tests/modeltests/update/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/update/models.py b/tests/modeltests/update/models.py new file mode 100644 index 0000000000..3b0f83389f --- /dev/null +++ b/tests/modeltests/update/models.py @@ -0,0 +1,67 @@ +""" +Tests for the update() queryset method that allows in-place, multi-object +updates. +""" + +from django.db import models + +class DataPoint(models.Model): + name = models.CharField(max_length=20) + value = models.CharField(max_length=20) + another_value = models.CharField(max_length=20, blank=True) + + def __unicode__(self): + return unicode(self.name) + +class RelatedPoint(models.Model): + name = models.CharField(max_length=20) + data = models.ForeignKey(DataPoint) + + def __unicode__(self): + return unicode(self.name) + + +__test__ = {'API_TESTS': """ +>>> DataPoint(name="d0", value="apple").save() +>>> DataPoint(name="d2", value="banana").save() +>>> d3 = DataPoint(name="d3", value="banana") +>>> d3.save() +>>> RelatedPoint(name="r1", data=d3).save() + +Objects are updated by first filtering the candidates into a queryset and then +calling the update() method. It executes immediately and returns nothing. + +>>> DataPoint.objects.filter(value="apple").update(name="d1") +>>> DataPoint.objects.filter(value="apple") +[] + +We can update multiple objects at once. + +>>> DataPoint.objects.filter(value="banana").update(value="pineapple") +>>> DataPoint.objects.get(name="d2").value +u'pineapple' + +Foreign key fields can also be updated, although you can only update the object +referred to, not anything inside the related object. + +>>> d = DataPoint.objects.get(name="d1") +>>> RelatedPoint.objects.filter(name="r1").update(data=d) +>>> RelatedPoint.objects.filter(data__name="d1") +[] + +Multiple fields can be updated at once + +>>> DataPoint.objects.filter(value="pineapple").update(value="fruit", another_value="peaches") +>>> d = DataPoint.objects.get(name="d2") +>>> d.value, d.another_value +(u'fruit', u'peaches') + +In the rare case you want to update every instance of a model, update() is also +a manager method. + +>>> DataPoint.objects.update(value='thing') +>>> DataPoint.objects.values('value').distinct() +[{'value': u'thing'}] + +""" +} diff --git a/tests/regressiontests/null_queries/models.py b/tests/regressiontests/null_queries/models.py index 2aa36b2c1a..df04f14eba 100644 --- a/tests/regressiontests/null_queries/models.py +++ b/tests/regressiontests/null_queries/models.py @@ -14,7 +14,7 @@ class Choice(models.Model): return u"Choice: %s in poll %s" % (self.choice, self.poll) __test__ = {'API_TESTS':""" -# Regression test for the use of None as a query value. None is interpreted as +# Regression test for the use of None as a query value. None is interpreted as # an SQL NULL, but only in __exact queries. # Set up some initial polls and choices >>> p1 = Poll(question='Why?') @@ -24,15 +24,20 @@ __test__ = {'API_TESTS':""" >>> c2 = Choice(poll=p1, choice='Why Not?') >>> c2.save() -# Exact query with value None returns nothing (=NULL in sql) ->>> Choice.objects.filter(id__exact=None) +# Exact query with value None returns nothing ("is NULL" in sql, but every 'id' +# field has a value). +>>> Choice.objects.filter(choice__exact=None) [] +Excluding the previous result returns everything. +>>> Choice.objects.exclude(choice=None).order_by('id') +[, ] + # Valid query, but fails because foo isn't a keyword ->>> Choice.objects.filter(foo__exact=None) +>>> Choice.objects.filter(foo__exact=None) Traceback (most recent call last): ... -TypeError: Cannot resolve keyword 'foo' into field. Choices are: id, poll, choice +FieldError: Cannot resolve keyword 'foo' into field. Choices are: choice, id, poll # Can't use None on anything other than __exact >>> Choice.objects.filter(id__gt=None) diff --git a/tests/regressiontests/queries/__init__.py b/tests/regressiontests/queries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py new file mode 100644 index 0000000000..483aa7218c --- /dev/null +++ b/tests/regressiontests/queries/models.py @@ -0,0 +1,658 @@ +""" +Various complex queries that have been problematic in the past. +""" + +import datetime + +from django.db import models +from django.db.models.query import Q + +class Tag(models.Model): + name = models.CharField(max_length=10) + parent = models.ForeignKey('self', blank=True, null=True) + + def __unicode__(self): + return self.name + +class Note(models.Model): + note = models.CharField(max_length=100) + misc = models.CharField(max_length=10) + + class Meta: + ordering = ['note'] + + def __unicode__(self): + return self.note + +class ExtraInfo(models.Model): + info = models.CharField(max_length=100) + note = models.ForeignKey(Note) + + class Meta: + ordering = ['info'] + + def __unicode__(self): + return self.info + +class Author(models.Model): + name = models.CharField(max_length=10) + num = models.IntegerField(unique=True) + extra = models.ForeignKey(ExtraInfo) + + def __unicode__(self): + return self.name + +class Item(models.Model): + name = models.CharField(max_length=10) + created = models.DateTimeField() + tags = models.ManyToManyField(Tag, blank=True, null=True) + creator = models.ForeignKey(Author) + note = models.ForeignKey(Note) + + class Meta: + ordering = ['-note', 'name'] + + def __unicode__(self): + return self.name + +class Report(models.Model): + name = models.CharField(max_length=10) + creator = models.ForeignKey(Author, to_field='num') + + def __unicode__(self): + return self.name + +class Ranking(models.Model): + rank = models.IntegerField() + author = models.ForeignKey(Author) + + class Meta: + # A complex ordering specification. Should stress the system a bit. + ordering = ('author__extra__note', 'author__name', 'rank') + + def __unicode__(self): + return '%d: %s' % (self.rank, self.author.name) + +class Cover(models.Model): + title = models.CharField(max_length=50) + item = models.ForeignKey(Item) + + class Meta: + ordering = ['item'] + + def __unicode__(self): + return self.title + +class Number(models.Model): + num = models.IntegerField() + + def __unicode__(self): + return unicode(self.num) + +# Some funky cross-linked models for testing a couple of infinite recursion +# cases. +class X(models.Model): + y = models.ForeignKey('Y') + +class Y(models.Model): + x1 = models.ForeignKey(X, related_name='y1') + +# Some models with a cycle in the default ordering. This would be bad if we +# didn't catch the infinite loop. +class LoopX(models.Model): + y = models.ForeignKey('LoopY') + + class Meta: + ordering = ['y'] + +class LoopY(models.Model): + x = models.ForeignKey(LoopX) + + class Meta: + ordering = ['x'] + +class LoopZ(models.Model): + z = models.ForeignKey('self') + + class Meta: + ordering = ['z'] + +__test__ = {'API_TESTS':""" +>>> t1 = Tag(name='t1') +>>> t1.save() +>>> t2 = Tag(name='t2', parent=t1) +>>> t2.save() +>>> t3 = Tag(name='t3', parent=t1) +>>> t3.save() +>>> t4 = Tag(name='t4', parent=t3) +>>> t4.save() +>>> t5 = Tag(name='t5', parent=t3) +>>> t5.save() + +>>> n1 = Note(note='n1', misc='foo') +>>> n1.save() +>>> n2 = Note(note='n2', misc='bar') +>>> n2.save() +>>> n3 = Note(note='n3', misc='foo') +>>> n3.save() + +Create these out of order so that sorting by 'id' will be different to sorting +by 'info'. Helps detect some problems later. +>>> e2 = ExtraInfo(info='e2', note=n2) +>>> e2.save() +>>> e1 = ExtraInfo(info='e1', note=n1) +>>> e1.save() + +>>> a1 = Author(name='a1', num=1001, extra=e1) +>>> a1.save() +>>> a2 = Author(name='a2', num=2002, extra=e1) +>>> a2.save() +>>> a3 = Author(name='a3', num=3003, extra=e2) +>>> a3.save() +>>> a4 = Author(name='a4', num=4004, extra=e2) +>>> a4.save() + +>>> time1 = datetime.datetime(2007, 12, 19, 22, 25, 0) +>>> time2 = datetime.datetime(2007, 12, 19, 21, 0, 0) +>>> time3 = datetime.datetime(2007, 12, 20, 22, 25, 0) +>>> time4 = datetime.datetime(2007, 12, 20, 21, 0, 0) +>>> i1 = Item(name='one', created=time1, creator=a1, note=n3) +>>> i1.save() +>>> i1.tags = [t1, t2] +>>> i2 = Item(name='two', created=time2, creator=a2, note=n2) +>>> i2.save() +>>> i2.tags = [t1, t3] +>>> i3 = Item(name='three', created=time3, creator=a2, note=n3) +>>> i3.save() +>>> i4 = Item(name='four', created=time4, creator=a4, note=n3) +>>> i4.save() +>>> i4.tags = [t4] + +>>> r1 = Report(name='r1', creator=a1) +>>> r1.save() +>>> r2 = Report(name='r2', creator=a3) +>>> r2.save() + +Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering +will be rank3, rank2, rank1. +>>> rank1 = Ranking(rank=2, author=a2) +>>> rank1.save() +>>> rank2 = Ranking(rank=1, author=a3) +>>> rank2.save() +>>> rank3 = Ranking(rank=3, author=a1) +>>> rank3.save() + +>>> c1 = Cover(title="first", item=i4) +>>> c1.save() +>>> c2 = Cover(title="second", item=i2) +>>> c2.save() + +>>> n1 = Number(num=4) +>>> n1.save() +>>> n2 = Number(num=8) +>>> n2.save() +>>> n3 = Number(num=12) +>>> n3.save() + +Bug #1050 +>>> Item.objects.filter(tags__isnull=True) +[] +>>> Item.objects.filter(tags__id__isnull=True) +[] + +Bug #1801 +>>> Author.objects.filter(item=i2) +[] +>>> Author.objects.filter(item=i3) +[] +>>> Author.objects.filter(item=i2) & Author.objects.filter(item=i3) +[] + +Bug #2306 +Checking that no join types are "left outer" joins. +>>> query = Item.objects.filter(tags=t2).query +>>> query.LOUTER not in [x[2] for x in query.alias_map.values()] +True + +>>> Item.objects.filter(Q(tags=t1)).order_by('name') +[, ] +>>> Item.objects.filter(Q(tags=t1)).filter(Q(tags=t2)) +[] +>>> Item.objects.filter(Q(tags=t1)).filter(Q(creator__name='fred')|Q(tags=t2)) +[] + +Each filter call is processed "at once" against a single table, so this is +different from the previous example as it tries to find tags that are two +things at once (rather than two tags). +>>> Item.objects.filter(Q(tags=t1) & Q(tags=t2)) +[] +>>> Item.objects.filter(Q(tags=t1), Q(creator__name='fred')|Q(tags=t2)) +[] + +>>> qs = Author.objects.filter(ranking__rank=2, ranking__id=rank1.id) +>>> list(qs) +[] +>>> qs.query.count_active_tables() +2 +>>> qs = Author.objects.filter(ranking__rank=2).filter(ranking__id=rank1.id) +>>> qs.query.count_active_tables() +3 + +Bug #4464 +>>> Item.objects.filter(tags=t1).filter(tags=t2) +[] +>>> Item.objects.filter(tags__in=[t1, t2]).distinct().order_by('name') +[, ] +>>> Item.objects.filter(tags__in=[t1, t2]).filter(tags=t3) +[] + +Bug #2080, #3592 +>>> Author.objects.filter(item__name='one') | Author.objects.filter(name='a3') +[, ] +>>> Author.objects.filter(Q(item__name='one') | Q(name='a3')) +[, ] +>>> Author.objects.filter(Q(name='a3') | Q(item__name='one')) +[, ] +>>> Author.objects.filter(Q(item__name='three') | Q(report__name='r3')) +[] + +Bug #4289 +A slight variation on the above theme: restricting the choices by the lookup +constraints. +>>> Number.objects.filter(num__lt=4) +[] +>>> Number.objects.filter(num__gt=8, num__lt=12) +[] +>>> Number.objects.filter(num__gt=8, num__lt=13) +[] +>>> Number.objects.filter(Q(num__lt=4) | Q(num__gt=8, num__lt=12)) +[] +>>> Number.objects.filter(Q(num__gt=8, num__lt=12) | Q(num__lt=4)) +[] +>>> Number.objects.filter(Q(num__gt=8) & Q(num__lt=12) | Q(num__lt=4)) +[] +>>> Number.objects.filter(Q(num__gt=7) & Q(num__lt=12) | Q(num__lt=4)) +[] + +Bug #6074 +Merging two empty result sets shouldn't leave a queryset with no constraints +(which would match everything). +>>> Author.objects.filter(Q(id__in=[])) +[] +>>> Author.objects.filter(Q(id__in=[])|Q(id__in=[])) +[] + +Bug #1878, #2939 +>>> Item.objects.values('creator').distinct().count() +3 + +# Create something with a duplicate 'name' so that we can test multi-column +# cases (which require some tricky SQL transformations under the covers). +>>> xx = Item(name='four', created=time1, creator=a2, note=n1) +>>> xx.save() +>>> Item.objects.exclude(name='two').values('creator', 'name').distinct().count() +4 +>>> Item.objects.exclude(name='two').extra(select={'foo': '%s'}, select_params=(1,)).values('creator', 'name', 'foo').distinct().count() +4 +>>> Item.objects.exclude(name='two').extra(select={'foo': '%s'}, select_params=(1,)).values('creator', 'name').distinct().count() +4 +>>> xx.delete() + +Bug #2253 +>>> q1 = Item.objects.order_by('name') +>>> q2 = Item.objects.filter(id=i1.id) +>>> q1 +[, , , ] +>>> q2 +[] +>>> (q1 | q2).order_by('name') +[, , , ] +>>> (q1 & q2).order_by('name') +[] + +# FIXME: This is difficult to fix and very much an edge case, so punt for now. +# # This is related to the order_by() tests, below, but the old bug exhibited +# # itself here (q2 was pulling too many tables into the combined query with the +# # new ordering, but only because we have evaluated q2 already). +# >>> len((q1 & q2).order_by('name').query.tables) +# 1 + +>>> q1 = Item.objects.filter(tags=t1) +>>> q2 = Item.objects.filter(note=n3, tags=t2) +>>> q3 = Item.objects.filter(creator=a4) +>>> ((q1 & q2) | q3).order_by('name') +[, ] + +Bugs #4088, #4306 +>>> Report.objects.filter(creator=1001) +[] +>>> Report.objects.filter(creator__num=1001) +[] +>>> Report.objects.filter(creator__id=1001) +[] +>>> Report.objects.filter(creator__id=a1.id) +[] +>>> Report.objects.filter(creator__name='a1') +[] + +Bug #4510 +>>> Author.objects.filter(report__name='r1') +[] + +Bug #5324, #6704 +>>> Item.objects.filter(tags__name='t4') +[] +>>> Item.objects.exclude(tags__name='t4').order_by('name').distinct() +[, , ] +>>> Item.objects.exclude(tags__name='t4').order_by('name').distinct().reverse() +[, , ] +>>> Author.objects.exclude(item__name='one').distinct().order_by('name') +[, , ] + + +# Excluding across a m2m relation when there is more than one related object +# associated was problematic. +>>> Item.objects.exclude(tags__name='t1').order_by('name') +[, ] +>>> Item.objects.exclude(tags__name='t1').exclude(tags__name='t4') +[] + +# Excluding from a relation that cannot be NULL should not use outer joins. +>>> query = Item.objects.exclude(creator__in=[a1, a2]).query +>>> query.LOUTER not in [x[2] for x in query.alias_map.values()] +True + +Similarly, when one of the joins cannot possibly, ever, involve NULL values (Author -> ExtraInfo, in the following), it should never be promoted to a left outer join. So hte following query should only involve one "left outer" join (Author -> Item is 0-to-many). +>>> qs = Author.objects.filter(id=a1.id).filter(Q(extra__note=n1)|Q(item__note=n3)) +>>> len([x[2] for x in qs.query.alias_map.values() if x[2] == query.LOUTER]) +1 + +The previous changes shouldn't affect nullable foreign key joins. +>>> Tag.objects.filter(parent__isnull=True).order_by('name') +[] +>>> Tag.objects.exclude(parent__isnull=True).order_by('name') +[, , , ] +>>> Tag.objects.exclude(Q(parent__name='t1') | Q(parent__isnull=True)).order_by('name') +[, ] +>>> Tag.objects.exclude(Q(parent__isnull=True) | Q(parent__name='t1')).order_by('name') +[, ] +>>> Tag.objects.exclude(Q(parent__parent__isnull=True)).order_by('name') +[, ] +>>> Tag.objects.filter(~Q(parent__parent__isnull=True)).order_by('name') +[, ] + +Bug #2091 +>>> t = Tag.objects.get(name='t4') +>>> Item.objects.filter(tags__in=[t]) +[] + +Combining querysets built on different models should behave in a well-defined +fashion. We raise an error. +>>> Author.objects.all() & Tag.objects.all() +Traceback (most recent call last): +... +AssertionError: Cannot combine queries on two different base models. +>>> Author.objects.all() | Tag.objects.all() +Traceback (most recent call last): +... +AssertionError: Cannot combine queries on two different base models. + +Bug #3141 +>>> Author.objects.extra(select={'foo': '1'}).count() +4 +>>> Author.objects.extra(select={'foo': '%s'}, select_params=(1,)).count() +4 + +Bug #2400 +>>> Author.objects.filter(item__isnull=True) +[] +>>> Tag.objects.filter(item__isnull=True) +[] + +Bug #2496 +>>> Item.objects.extra(tables=['queries_author']).select_related().order_by('name')[:1] +[] + +Bug #2076 +# Ordering on related tables should be possible, even if the table is not +# otherwise involved. +>>> Item.objects.order_by('note__note', 'name') +[, , , ] + +# Ordering on a related field should use the remote model's default ordering as +# a final step. +>>> Author.objects.order_by('extra', '-name') +[, , , ] + +# Using remote model default ordering can span multiple models (in this case, +# Cover is ordered by Item's default, which uses Note's default). +>>> Cover.objects.all() +[, ] + +# If you're not careful, it's possible to introduce infinite loops via default +# ordering on foreign keys in a cycle. We detect that. +>>> LoopX.objects.all() +Traceback (most recent call last): +... +FieldError: Infinite loop caused by ordering. + +>>> LoopZ.objects.all() +Traceback (most recent call last): +... +FieldError: Infinite loop caused by ordering. + +# ... but you can still order in a non-recursive fashion amongst linked fields +# (the previous test failed because the default ordering was recursive). +>>> LoopX.objects.all().order_by('y__x__y__x__id') +[] + +# If the remote model does not have a default ordering, we order by its 'id' +# field. +>>> Item.objects.order_by('creator', 'name') +[, , , ] + +# Cross model ordering is possible in Meta, too. +>>> Ranking.objects.all() +[, , ] +>>> Ranking.objects.all().order_by('rank') +[, , ] + +# Ordering by a many-valued attribute (e.g. a many-to-many or reverse +# ForeignKey) is legal, but the results might not make sense. That isn't +# Django's problem. Garbage in, garbage out. +>>> Item.objects.all().order_by('tags', 'id') +[, , , , ] + +# If we replace the default ordering, Django adjusts the required tables +# automatically. Item normally requires a join with Note to do the default +# ordering, but that isn't needed here. +>>> qs = Item.objects.order_by('name') +>>> qs +[, , , ] +>>> len(qs.query.tables) +1 + +# Ordering of extra() pieces is possible, too and you can mix extra fields and +# model fields in the ordering. +>>> Ranking.objects.extra(tables=['django_site'], order_by=['-django_site.id', 'rank']) +[, , ] + +>>> qs = Ranking.objects.extra(select={'good': 'case when rank > 2 then 1 else 0 end'}) +>>> [o.good for o in qs.extra(order_by=('-good',))] == [True, False, False] +True +>>> qs.extra(order_by=('-good', 'id')) +[, , ] + +# Despite having some extra aliases in the query, we can still omit them in a +# values() query. +>>> qs.values('id', 'rank').order_by('id') +[{'id': 1, 'rank': 2}, {'id': 2, 'rank': 1}, {'id': 3, 'rank': 3}] + +Bugs #2874, #3002 +>>> qs = Item.objects.select_related().order_by('note__note', 'name') +>>> list(qs) +[, , , ] + +# This is also a good select_related() test because there are multiple Note +# entries in the SQL. The two Note items should be different. +>>> qs[0].note, qs[0].creator.extra.note +(, ) + +Bug #3037 +>>> Item.objects.filter(Q(creator__name='a3', name='two')|Q(creator__name='a4', name='four')) +[] + +Bug #5321, #7070 + +Ordering columns must be included in the output columns. Note that this means +results that might otherwise be distinct are not (if there are multiple values +in the ordering cols), as in this example. This isn't a bug; it's a warning to +be careful with the selection of ordering columns. + +>>> Note.objects.values('misc').distinct().order_by('note', '-misc') +[{'misc': u'foo'}, {'misc': u'bar'}, {'misc': u'foo'}] + +Bug #4358 +If you don't pass any fields to values(), relation fields are returned as +"foo_id" keys, not "foo". For consistency, you should be able to pass "foo_id" +in the fields list and have it work, too. We actually allow both "foo" and +"foo_id". + +# The *_id version is returned by default. +>>> 'note_id' in ExtraInfo.objects.values()[0] +True + +# You can also pass it in explicitly. +>>> ExtraInfo.objects.values('note_id') +[{'note_id': 1}, {'note_id': 2}] + +# ...or use the field name. +>>> ExtraInfo.objects.values('note') +[{'note': 1}, {'note': 2}] + +Bug #5261 +>>> Note.objects.exclude(Q()) +[, , ] + +Bug #3045, #3288 +Once upon a time, select_related() with circular relations would loop +infinitely if you forgot to specify "depth". Now we set an arbitrary default +upper bound. +>>> X.objects.all() +[] +>>> X.objects.select_related() +[] + +Bug #3739 +The all() method on querysets returns a copy of the queryset. +>>> q1 = Item.objects.order_by('name') +>>> id(q1) == id(q1.all()) +False + +Bug #2902 +Parameters can be given to extra_select, *if* you use a SortedDict. + +(First we need to know which order the keys fall in "naturally" on your system, +so we can put things in the wrong way around from normal. A normal dict would +thus fail.) +>>> from django.utils.datastructures import SortedDict +>>> s = [('a', '%s'), ('b', '%s')] +>>> params = ['one', 'two'] +>>> if {'a': 1, 'b': 2}.keys() == ['a', 'b']: +... s.reverse() +... params.reverse() + +# This slightly odd comparison works aorund the fact that PostgreSQL will +# return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of +# using constants here and not a real concern. +>>> d = Item.objects.extra(select=SortedDict(s), select_params=params).values('a', 'b')[0] +>>> d == {'a': u'one', 'b': u'two'} +True + +# Order by the number of tags attached to an item. +>>> l = Item.objects.extra(select={'count': 'select count(*) from queries_item_tags where queries_item_tags.item_id = queries_item.id'}).order_by('-count') +>>> [o.count for o in l] +[2, 2, 1, 0] + +Bug #6154 +Multiple filter statements are joined using "AND" all the time. + +>>> Author.objects.filter(id=a1.id).filter(Q(extra__note=n1)|Q(item__note=n3)) +[] +>>> Author.objects.filter(Q(extra__note=n1)|Q(item__note=n3)).filter(id=a1.id) +[] + +Bug #6981 +>>> Tag.objects.select_related('parent').order_by('name') +[, , , , ] + +Bug #6180, #6203 -- dates with limits and/or counts +>>> Item.objects.count() +4 +>>> Item.objects.dates('created', 'month').count() +1 +>>> Item.objects.dates('created', 'day').count() +2 +>>> len(Item.objects.dates('created', 'day')) +2 +>>> Item.objects.dates('created', 'day')[0] +datetime.datetime(2007, 12, 19, 0, 0) + +Bug #7087 -- dates with extra select columns +>>> Item.objects.dates('created', 'day').extra(select={'a': 1}) +[datetime.datetime(2007, 12, 19, 0, 0), datetime.datetime(2007, 12, 20, 0, 0)] + +Test that parallel iterators work. + +>>> qs = Tag.objects.all() +>>> i1, i2 = iter(qs), iter(qs) +>>> i1.next(), i1.next() +(, ) +>>> i2.next(), i2.next(), i2.next() +(, , ) +>>> i1.next() + + +>>> qs = X.objects.all() +>>> bool(qs) +False +>>> bool(qs) +False + +We can do slicing beyond what is currently in the result cache, too. + +## FIXME!! This next test causes really weird PostgreSQL behaviour, but it's +## only apparent much later when the full test suite runs. I don't understand +## what's going on here yet. +## +## # We need to mess with the implemenation internals a bit here to decrease the +## # cache fill size so that we don't read all the results at once. +## >>> from django.db.models import query +## >>> query.ITER_CHUNK_SIZE = 2 +## >>> qs = Tag.objects.all() +## +## # Fill the cache with the first chunk. +## >>> bool(qs) +## True +## >>> len(qs._result_cache) +## 2 +## +## # Query beyond the end of the cache and check that it is filled out as required. +## >>> qs[4] +## +## >>> len(qs._result_cache) +## 5 +## +## # But querying beyond the end of the result set will fail. +## >>> qs[100] +## Traceback (most recent call last): +## ... +## IndexError: ... + +Bug #7045 -- extra tables used to crash SQL construction on the second use. +>>> qs = Ranking.objects.extra(tables=['django_site']) +>>> s = qs.query.as_sql() +>>> s = qs.query.as_sql() # test passes if this doesn't raise an exception. + +"""} + diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py index e9df508822..593e61ecc7 100644 --- a/tests/regressiontests/serializers_regress/models.py +++ b/tests/regressiontests/serializers_regress/models.py @@ -77,7 +77,7 @@ class USStateData(models.Model): class XMLData(models.Model): data = models.XMLField(null=True) - + class Tag(models.Model): """A tag on an item.""" data = models.SlugField() @@ -93,40 +93,39 @@ class GenericData(models.Model): data = models.CharField(max_length=30) tags = generic.GenericRelation(Tag) - + # The following test classes are all for validation # of related objects; in particular, forward, backward, # and self references. - + class Anchor(models.Model): - """This is a model that can be used as + """This is a model that can be used as something for other models to point at""" - + data = models.CharField(max_length=30) class UniqueAnchor(models.Model): - """This is a model that can be used as + """This is a model that can be used as something for other models to point at""" data = models.CharField(unique=True, max_length=30) - + class FKData(models.Model): data = models.ForeignKey(Anchor, null=True) - + class M2MData(models.Model): data = models.ManyToManyField(Anchor, null=True) - + class O2OData(models.Model): - # One to one field can't be null, since it is a PK. - data = models.OneToOneField(Anchor) + # One to one field can't be null here, since it is a PK. + data = models.OneToOneField(Anchor, primary_key=True) class FKSelfData(models.Model): data = models.ForeignKey('self', null=True) - + class M2MSelfData(models.Model): data = models.ManyToManyField('self', null=True, symmetrical=False) - class FKDataToField(models.Model): data = models.ForeignKey(UniqueAnchor, null=True, to_field='data') @@ -142,7 +141,7 @@ class FKDataToO2O(models.Model): class BooleanPKData(models.Model): data = models.BooleanField(primary_key=True) - + class CharPKData(models.Model): data = models.CharField(max_length=30, primary_key=True) diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 24111308d7..db34f8cf77 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -31,13 +31,13 @@ except ImportError: def data_create(pk, klass, data): instance = klass(id=pk) instance.data = data - models.Model.save(instance, raw=True) + models.Model.save_base(instance, raw=True) return instance def generic_create(pk, klass, data): instance = klass(id=pk) instance.data = data[0] - models.Model.save(instance, raw=True) + models.Model.save_base(instance, raw=True) for tag in data[1:]: instance.tags.create(data=tag) return instance @@ -45,25 +45,25 @@ def generic_create(pk, klass, data): def fk_create(pk, klass, data): instance = klass(id=pk) setattr(instance, 'data_id', data) - models.Model.save(instance, raw=True) + models.Model.save_base(instance, raw=True) return instance def m2m_create(pk, klass, data): instance = klass(id=pk) - models.Model.save(instance, raw=True) + models.Model.save_base(instance, raw=True) instance.data = data return instance def o2o_create(pk, klass, data): instance = klass() instance.data_id = data - models.Model.save(instance, raw=True) + models.Model.save_base(instance, raw=True) return instance def pk_create(pk, klass, data): instance = klass() instance.data = data - models.Model.save(instance, raw=True) + models.Model.save_base(instance, raw=True) return instance # A set of functions that can be used to compare @@ -309,7 +309,7 @@ def fieldsTest(format, self): management.call_command('flush', verbosity=0, interactive=False) obj = ComplexModel(field1='first',field2='second',field3='third') - obj.save(raw=True) + obj.save_base(raw=True) # Serialize then deserialize the test database serialized_data = serializers.serialize(format, [obj], indent=2, fields=('field1','field3')) @@ -325,7 +325,7 @@ def streamTest(format, self): management.call_command('flush', verbosity=0, interactive=False) obj = ComplexModel(field1='first',field2='second',field3='third') - obj.save(raw=True) + obj.save_base(raw=True) # Serialize the test database to a stream stream = StringIO()