diff options
| author | Marc Garcia <garcia.marc@gmail.com> | 2009-06-07 00:30:56 +0000 |
|---|---|---|
| committer | Marc Garcia <garcia.marc@gmail.com> | 2009-06-07 00:30:56 +0000 |
| commit | f973c57aaac3e918ac0234acbbbc86834ccfca86 (patch) | |
| tree | dcffd814d5881868ac72c01cabc220c4c4995ccb | |
| parent | 3860c69bad92e4a514b1d10426445a00f3e9239a (diff) | |
Merged up to trunk r10934
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/i18n-improvements@10935 bcc190cf-cafb-0310-a4f2-bffc1f526a37
28 files changed, 1493 insertions, 485 deletions
diff --git a/django/conf/locale/es_AR/LC_MESSAGES/django.mo b/django/conf/locale/es_AR/LC_MESSAGES/django.mo Binary files differindex e4474b0eb8..57349e8d9b 100644 --- a/django/conf/locale/es_AR/LC_MESSAGES/django.mo +++ b/django/conf/locale/es_AR/LC_MESSAGES/django.mo diff --git a/django/conf/locale/es_AR/LC_MESSAGES/django.po b/django/conf/locale/es_AR/LC_MESSAGES/django.po index f92735adc6..d6726ab57b 100644 --- a/django/conf/locale/es_AR/LC_MESSAGES/django.po +++ b/django/conf/locale/es_AR/LC_MESSAGES/django.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2008-11-13 18:20-0200\n" -"PO-Revision-Date: 2008-11-13 18:27-0200\n" +"POT-Creation-Date: 2009-05-28 12:30-0300\n" +"PO-Revision-Date: 2009-05-05 20:35-0300\n" "Last-Translator: Ramiro Morales <rm0@gmx.net>\n" "Language-Team: Django-I18N <django-i18n@googlegroups.com>\n" "MIME-Version: 1.0\n" @@ -100,121 +100,139 @@ msgid "Hebrew" msgstr "hebreo" #: conf/global_settings.py:65 +msgid "Hindi" +msgstr "Hindi" + +#: conf/global_settings.py:66 msgid "Croatian" msgstr "croata" -#: conf/global_settings.py:66 +#: conf/global_settings.py:67 msgid "Icelandic" msgstr "islandés" -#: conf/global_settings.py:67 +#: conf/global_settings.py:68 msgid "Italian" msgstr "italiano" -#: conf/global_settings.py:68 +#: conf/global_settings.py:69 msgid "Japanese" msgstr "japonés" -#: conf/global_settings.py:69 +#: conf/global_settings.py:70 msgid "Georgian" msgstr "georgiano" -#: conf/global_settings.py:70 +#: conf/global_settings.py:71 msgid "Korean" msgstr "koreano" -#: conf/global_settings.py:71 +#: conf/global_settings.py:72 msgid "Khmer" msgstr "jémer" -#: conf/global_settings.py:72 +#: conf/global_settings.py:73 msgid "Kannada" msgstr "canarés" -#: conf/global_settings.py:73 +#: conf/global_settings.py:74 msgid "Latvian" msgstr "letón" -#: conf/global_settings.py:74 +#: conf/global_settings.py:75 msgid "Lithuanian" msgstr "lituano" -#: conf/global_settings.py:75 +#: conf/global_settings.py:76 msgid "Macedonian" msgstr "macedonio" -#: conf/global_settings.py:76 +#: conf/global_settings.py:77 msgid "Dutch" msgstr "holandés" -#: conf/global_settings.py:77 +#: conf/global_settings.py:78 msgid "Norwegian" msgstr "noruego" -#: conf/global_settings.py:78 +#: conf/global_settings.py:79 msgid "Polish" msgstr "polaco" -#: conf/global_settings.py:79 +#: conf/global_settings.py:80 msgid "Portuguese" msgstr "portugués" -#: conf/global_settings.py:80 +#: conf/global_settings.py:81 msgid "Brazilian Portuguese" msgstr "portugués de Brasil" -#: conf/global_settings.py:81 +#: conf/global_settings.py:82 msgid "Romanian" msgstr "rumano" -#: conf/global_settings.py:82 +#: conf/global_settings.py:83 msgid "Russian" msgstr "ruso" -#: conf/global_settings.py:83 +#: conf/global_settings.py:84 msgid "Slovak" msgstr "eslovaco" -#: conf/global_settings.py:84 +#: conf/global_settings.py:85 msgid "Slovenian" msgstr "esloveno" -#: conf/global_settings.py:85 +#: conf/global_settings.py:86 msgid "Serbian" msgstr "serbio" -#: conf/global_settings.py:86 +#: conf/global_settings.py:87 msgid "Swedish" msgstr "sueco" -#: conf/global_settings.py:87 +#: conf/global_settings.py:88 msgid "Tamil" msgstr "tamil" -#: conf/global_settings.py:88 +#: conf/global_settings.py:89 msgid "Telugu" msgstr "telugu" -#: conf/global_settings.py:89 +#: conf/global_settings.py:90 msgid "Thai" msgstr "tailandés" -#: conf/global_settings.py:90 +#: conf/global_settings.py:91 msgid "Turkish" msgstr "turco" -#: conf/global_settings.py:91 +#: conf/global_settings.py:92 msgid "Ukrainian" msgstr "ucraniano" -#: conf/global_settings.py:92 +#: conf/global_settings.py:93 msgid "Simplified Chinese" msgstr "chino simplificado" -#: conf/global_settings.py:93 +#: conf/global_settings.py:94 msgid "Traditional Chinese" msgstr "chino tradicional" +#: contrib/admin/actions.py:60 +#, python-format +msgid "Successfully deleted %(count)d %(items)s." +msgstr "Se eliminaron con éxito %(count)d %(items)s." + +#: contrib/admin/actions.py:67 contrib/admin/options.py:1025 +msgid "Are you sure?" +msgstr "¿Está seguro?" + +#: contrib/admin/actions.py:85 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "Eliminar %(verbose_name_plural)s seleccionados/as" + #: contrib/admin/filterspecs.py:44 #, python-format msgid "" @@ -224,43 +242,47 @@ msgstr "" "<h3>Por %s:</h3>\n" "<ul>\n" -#: contrib/admin/filterspecs.py:74 contrib/admin/filterspecs.py:91 -#: contrib/admin/filterspecs.py:146 contrib/admin/filterspecs.py:172 +#: contrib/admin/filterspecs.py:75 contrib/admin/filterspecs.py:92 +#: contrib/admin/filterspecs.py:147 contrib/admin/filterspecs.py:173 msgid "All" msgstr "Todos/as" -#: contrib/admin/filterspecs.py:112 +#: contrib/admin/filterspecs.py:113 msgid "Any date" msgstr "Cualquier fecha" -#: contrib/admin/filterspecs.py:113 +#: contrib/admin/filterspecs.py:114 msgid "Today" msgstr "Hoy" -#: contrib/admin/filterspecs.py:116 +#: contrib/admin/filterspecs.py:117 msgid "Past 7 days" msgstr "Últimos 7 días" -#: contrib/admin/filterspecs.py:118 +#: contrib/admin/filterspecs.py:119 msgid "This month" msgstr "Este mes" -#: contrib/admin/filterspecs.py:120 +#: contrib/admin/filterspecs.py:121 msgid "This year" msgstr "Este año" -#: contrib/admin/filterspecs.py:146 forms/widgets.py:390 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:434 msgid "Yes" msgstr "Sí" -#: contrib/admin/filterspecs.py:146 forms/widgets.py:390 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:434 msgid "No" msgstr "No" -#: contrib/admin/filterspecs.py:153 forms/widgets.py:390 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:434 msgid "Unknown" msgstr "Desconocido" +#: contrib/admin/helpers.py:14 +msgid "Action:" +msgstr "Acción:" + #: contrib/admin/models.py:19 msgid "action time" msgstr "hora de la acción" @@ -289,60 +311,61 @@ msgstr "entrada de registro" msgid "log entries" msgstr "entradas de registro" -#: contrib/admin/options.py:60 contrib/admin/options.py:121 +#: contrib/admin/options.py:133 contrib/admin/options.py:147 msgid "None" msgstr "Ninguno" -#: contrib/admin/options.py:338 +#: contrib/admin/options.py:519 #, python-format msgid "Changed %s." msgstr "Modifica %s." -#: contrib/admin/options.py:338 contrib/admin/options.py:348 -#: contrib/comments/templates/comments/preview.html:15 forms/models.py:288 +#: contrib/admin/options.py:519 contrib/admin/options.py:529 +#: contrib/comments/templates/comments/preview.html:16 forms/models.py:388 +#: forms/models.py:587 msgid "and" msgstr "y" -#: contrib/admin/options.py:343 +#: contrib/admin/options.py:524 #, python-format msgid "Added %(name)s \"%(object)s\"." msgstr "Se agregó %(name)s \"%(object)s\"." -#: contrib/admin/options.py:347 +#: contrib/admin/options.py:528 #, python-format msgid "Changed %(list)s for %(name)s \"%(object)s\"." msgstr "Se modificaron %(list)s en %(name)s \"%(object)s\"." -#: contrib/admin/options.py:352 +#: contrib/admin/options.py:533 #, python-format msgid "Deleted %(name)s \"%(object)s\"." msgstr "Se eliminó %(name)s \"%(object)s\"." -#: contrib/admin/options.py:356 +#: contrib/admin/options.py:537 msgid "No fields changed." msgstr "No ha modificado ningún campo." -#: contrib/admin/options.py:417 contrib/auth/admin.py:51 +#: contrib/admin/options.py:598 contrib/auth/admin.py:67 #, python-format msgid "The %(name)s \"%(obj)s\" was added successfully." msgstr "Se agregó con éxito %(name)s \"%(obj)s\"." -#: contrib/admin/options.py:421 contrib/admin/options.py:454 -#: contrib/auth/admin.py:59 +#: contrib/admin/options.py:602 contrib/admin/options.py:635 +#: contrib/auth/admin.py:75 msgid "You may edit it again below." msgstr "Puede modificarlo/a nuevamente abajo." -#: contrib/admin/options.py:431 contrib/admin/options.py:464 +#: contrib/admin/options.py:612 contrib/admin/options.py:645 #, python-format msgid "You may add another %s below." msgstr "Puede agregar otro/a %s abajo." -#: contrib/admin/options.py:452 +#: contrib/admin/options.py:633 #, python-format msgid "The %(name)s \"%(obj)s\" was changed successfully." msgstr "Se modificó con éxito %(name)s \"%(obj)s\"." -#: contrib/admin/options.py:460 +#: contrib/admin/options.py:641 #, python-format msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." @@ -350,40 +373,43 @@ msgstr "" "Se agregó con éxito %(name)s \"%(obj)s\". Puede modificarlo/a nuevamente " "abajo." -#: contrib/admin/options.py:528 +#: contrib/admin/options.py:772 #, python-format msgid "Add %s" msgstr "Agregar %s" -#: contrib/admin/options.py:559 contrib/admin/options.py:673 +#: contrib/admin/options.py:803 contrib/admin/options.py:1003 #, python-format msgid "%(name)s object with primary key %(key)r does not exist." msgstr "No existe un objeto %(name)s con una clave primaria %(key)r." -#: contrib/admin/options.py:606 +#: contrib/admin/options.py:860 #, python-format msgid "Change %s" msgstr "Modificar %s" -#: contrib/admin/options.py:638 +#: contrib/admin/options.py:904 msgid "Database error" msgstr "Error de base de datos" -#: contrib/admin/options.py:688 +#: contrib/admin/options.py:940 +#, python-format +msgid "%(count)s %(name)s was changed successfully." +msgid_plural "%(count)s %(name)s were changed successfully." +msgstr[0] "Se ha modificado con éxito %(count)s %(name)s." +msgstr[1] "Se han modificado con éxito %(count)s %(name)s." + +#: contrib/admin/options.py:1018 #, python-format msgid "The %(name)s \"%(obj)s\" was deleted successfully." msgstr "Se eliminó con éxito %(name)s \"%(obj)s\"." -#: contrib/admin/options.py:695 -msgid "Are you sure?" -msgstr "¿Está seguro?" - -#: contrib/admin/options.py:724 +#: contrib/admin/options.py:1054 #, python-format msgid "Change history: %s" msgstr "Historia de modificaciones: %s" -#: contrib/admin/sites.py:16 contrib/admin/views/decorators.py:14 +#: contrib/admin/sites.py:20 contrib/admin/views/decorators.py:14 #: contrib/auth/forms.py:80 msgid "" "Please enter a correct username and password. Note that both fields are case-" @@ -392,11 +418,11 @@ msgstr "" "Por favor introduzca un nombre de usuario y una contraseña correctos. Note " "que ambos campos son sensibles a mayúsculas/minúsculas." -#: contrib/admin/sites.py:226 contrib/admin/views/decorators.py:40 +#: contrib/admin/sites.py:278 contrib/admin/views/decorators.py:40 msgid "Please log in again, because your session has expired." msgstr "Por favor, identifíquese de nuevo porque su sesión ha caducado." -#: contrib/admin/sites.py:233 contrib/admin/views/decorators.py:47 +#: contrib/admin/sites.py:285 contrib/admin/views/decorators.py:47 msgid "" "Looks like your browser isn't configured to accept cookies. Please enable " "cookies, reload this page, and try again." @@ -404,64 +430,64 @@ msgstr "" "Parece que su navegador no está configurado para aceptar cookies. Por favor " "actívelas, recargue esta página, e inténtelo de nuevo." -#: contrib/admin/sites.py:249 contrib/admin/sites.py:255 +#: contrib/admin/sites.py:301 contrib/admin/sites.py:307 #: contrib/admin/views/decorators.py:66 msgid "Usernames cannot contain the '@' character." msgstr "Los nombres de usuario no pueden contener el carácter '@'." -#: contrib/admin/sites.py:252 contrib/admin/views/decorators.py:62 +#: contrib/admin/sites.py:304 contrib/admin/views/decorators.py:62 #, python-format msgid "Your e-mail address is not your username. Try '%s' instead." msgstr "" "Su dirección de correo electrónico no es su nombre de usuario. Intente " "nuevamente usando '%s'." -#: contrib/admin/sites.py:312 +#: contrib/admin/sites.py:360 msgid "Site administration" msgstr "Administración de sitio" -#: contrib/admin/sites.py:325 contrib/admin/templates/admin/login.html:26 +#: contrib/admin/sites.py:373 contrib/admin/templates/admin/login.html:26 #: contrib/admin/templates/registration/password_reset_complete.html:14 #: contrib/admin/views/decorators.py:20 msgid "Log in" msgstr "Identificarse" -#: contrib/admin/sites.py:372 +#: contrib/admin/sites.py:417 #, python-format msgid "%s administration" msgstr "Administración de %s" -#: contrib/admin/util.py:138 +#: contrib/admin/util.py:168 #, python-format msgid "One or more %(fieldname)s in %(name)s: %(obj)s" msgstr "Uno o más %(fieldname)s en %(name)s: %(obj)s" -#: contrib/admin/util.py:143 +#: contrib/admin/util.py:173 #, python-format msgid "One or more %(fieldname)s in %(name)s:" msgstr "Uno o más %(fieldname)s en %(name)s:" -#: contrib/admin/widgets.py:70 +#: contrib/admin/widgets.py:71 msgid "Date:" msgstr "Fecha:" -#: contrib/admin/widgets.py:70 +#: contrib/admin/widgets.py:71 msgid "Time:" msgstr "Hora:" -#: contrib/admin/widgets.py:94 +#: contrib/admin/widgets.py:95 msgid "Currently:" msgstr "Actualmente" -#: contrib/admin/widgets.py:94 +#: contrib/admin/widgets.py:95 msgid "Change:" msgstr "Modificar:" -#: contrib/admin/widgets.py:123 +#: contrib/admin/widgets.py:124 msgid "Lookup" msgstr "Buscar" -#: contrib/admin/widgets.py:230 +#: contrib/admin/widgets.py:236 msgid "Add Another" msgstr "Agregar otro/a" @@ -478,8 +504,9 @@ msgstr "Lo sentimos, pero no se encuentra la página solicitada." #: contrib/admin/templates/admin/app_index.html:8 #: contrib/admin/templates/admin/base.html:31 #: contrib/admin/templates/admin/change_form.html:17 -#: contrib/admin/templates/admin/change_list.html:8 +#: contrib/admin/templates/admin/change_list.html:25 #: contrib/admin/templates/admin/delete_confirmation.html:6 +#: contrib/admin/templates/admin/delete_selected_confirmation.html:6 #: contrib/admin/templates/admin/invalid_setup.html:4 #: contrib/admin/templates/admin/object_history.html:6 #: contrib/admin/templates/admin/auth/user/change_password.html:10 @@ -515,6 +542,14 @@ msgstr "" "mediante correo electrónico y debería ser solucionado en breve. Gracias por " "su paciencia." +#: contrib/admin/templates/admin/actions.html:4 +msgid "Run the selected action" +msgstr "Ejecutar la acción seleccionada" + +#: contrib/admin/templates/admin/actions.html:4 +msgid "Go" +msgstr "Ejecutar" + #: contrib/admin/templates/admin/app_index.html:10 #: contrib/admin/templates/admin/index.html:19 #, python-format @@ -533,8 +568,8 @@ msgid "Documentation" msgstr "Documentación" #: contrib/admin/templates/admin/base.html:26 -#: contrib/admin/templates/admin/auth/user/change_password.html:13 -#: contrib/admin/templates/admin/auth/user/change_password.html:46 +#: contrib/admin/templates/admin/auth/user/change_password.html:14 +#: contrib/admin/templates/admin/auth/user/change_password.html:47 #: contrib/admin/templates/registration/password_change_done.html:3 #: contrib/admin/templates/registration/password_change_form.html:3 msgid "Change password" @@ -571,23 +606,24 @@ msgid "View on site" msgstr "Ver en el sitio" #: contrib/admin/templates/admin/change_form.html:38 -#: contrib/admin/templates/admin/auth/user/change_password.html:22 +#: contrib/admin/templates/admin/change_list.html:54 +#: contrib/admin/templates/admin/auth/user/change_password.html:23 msgid "Please correct the error below." msgid_plural "Please correct the errors below." msgstr[0] "Por favor, corrija el siguiente error." msgstr[1] "Por favor, corrija los siguientes errores." -#: contrib/admin/templates/admin/change_list.html:16 +#: contrib/admin/templates/admin/change_list.html:46 #, python-format msgid "Add %(name)s" msgstr "Agregar %(name)s" -#: contrib/admin/templates/admin/change_list.html:26 +#: contrib/admin/templates/admin/change_list.html:65 msgid "Filter" msgstr "Filtrar" #: contrib/admin/templates/admin/delete_confirmation.html:10 -#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:246 +#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:275 msgid "Delete" msgstr "Eliminar" @@ -612,9 +648,34 @@ msgstr "" "\"? Se eliminarán los siguientes objetos relacionados:" #: contrib/admin/templates/admin/delete_confirmation.html:28 +#: contrib/admin/templates/admin/delete_selected_confirmation.html:33 msgid "Yes, I'm sure" msgstr "Sí, estoy seguro" +#: contrib/admin/templates/admin/delete_selected_confirmation.html:9 +msgid "Delete multiple objects" +msgstr "Eliminar múltiples objetos" + +#: contrib/admin/templates/admin/delete_selected_confirmation.html:15 +#, python-format +msgid "" +"Deleting the %(object_name)s would result in deleting related objects, but " +"your account doesn't have permission to delete the following types of " +"objects:" +msgstr "" +"El eliminar %(object_name)s provocaría la eliminación de objetos " +"relacionados, pero su cuenta no tiene permiso para eliminar los siguientes " +"tipos de objetos:" + +#: contrib/admin/templates/admin/delete_selected_confirmation.html:22 +#, python-format +msgid "" +"Are you sure you want to delete the selected %(object_name)s objects? All of " +"the following objects and it's related items will be deleted:" +msgstr "" +"¿Está seguro de que quiere eliminar los objetos %(object_name)s? Se " +"eliminarán todos los siguientes objetos relacionados:" + #: contrib/admin/templates/admin/filter.html:2 #, python-format msgid " By %(filter_title)s " @@ -645,6 +706,10 @@ msgstr "Mis acciones" msgid "None available" msgstr "Ninguna disponible" +#: contrib/admin/templates/admin/index.html:72 +msgid "Unknown content" +msgstr "COntenido desconocido" + #: contrib/admin/templates/admin/invalid_setup.html:7 msgid "" "Something's wrong with your database installation. Make sure the appropriate " @@ -677,7 +742,7 @@ msgid "Action" msgstr "Acción" #: contrib/admin/templates/admin/object_history.html:30 -#: utils/translation/trans_real.py:404 +#: utils/translation/trans_real.py:400 msgid "DATETIME_FORMAT" msgstr "j N Y P" @@ -694,7 +759,7 @@ msgid "Show all" msgstr "Mostrar todos/as" #: contrib/admin/templates/admin/search_form.html:8 -msgid "Go" +msgid "Search" msgstr "Buscar" #: contrib/admin/templates/admin/search_form.html:10 @@ -739,24 +804,24 @@ msgid "Username" msgstr "Nombre de usuario:" #: contrib/admin/templates/admin/auth/user/add_form.html:20 -#: contrib/admin/templates/admin/auth/user/change_password.html:33 -#: contrib/auth/forms.py:17 contrib/auth/forms.py:60 contrib/auth/forms.py:184 +#: contrib/admin/templates/admin/auth/user/change_password.html:34 +#: contrib/auth/forms.py:17 contrib/auth/forms.py:60 contrib/auth/forms.py:185 msgid "Password" msgstr "Contraseña:" #: contrib/admin/templates/admin/auth/user/add_form.html:26 -#: contrib/admin/templates/admin/auth/user/change_password.html:39 -#: contrib/auth/forms.py:185 +#: contrib/admin/templates/admin/auth/user/change_password.html:40 +#: contrib/auth/forms.py:186 msgid "Password (again)" msgstr "Contraseña (de nuevo)" #: contrib/admin/templates/admin/auth/user/add_form.html:27 -#: contrib/admin/templates/admin/auth/user/change_password.html:40 +#: contrib/admin/templates/admin/auth/user/change_password.html:41 msgid "Enter the same password as above, for verification." msgstr "" "Para verificación, introduzca la misma contraseña que introdujo arriba." -#: contrib/admin/templates/admin/auth/user/change_password.html:26 +#: contrib/admin/templates/admin/auth/user/change_password.html:27 #, python-format msgid "Enter a new password for the user <strong>%(username)s</strong>." msgstr "" @@ -925,166 +990,166 @@ msgstr "Dirección de correo electrónico:" msgid "Reset my password" msgstr "Recuperar mi contraseña" -#: contrib/admin/templatetags/admin_list.py:284 +#: contrib/admin/templatetags/admin_list.py:299 msgid "All dates" msgstr "Todas las fechas" -#: contrib/admin/views/main.py:69 +#: contrib/admin/views/main.py:70 #, python-format msgid "Select %s" msgstr "Seleccione %s" -#: contrib/admin/views/main.py:69 +#: contrib/admin/views/main.py:70 #, python-format msgid "Select %s to change" msgstr "Seleccione %s a modificar" -#: contrib/admin/views/template.py:36 contrib/sites/models.py:38 +#: contrib/admin/views/template.py:37 contrib/sites/models.py:38 msgid "site" msgstr "sitio" -#: contrib/admin/views/template.py:38 +#: contrib/admin/views/template.py:39 msgid "template" msgstr "plantilla" -#: contrib/admindocs/views.py:57 contrib/admindocs/views.py:59 -#: contrib/admindocs/views.py:61 +#: contrib/admindocs/views.py:58 contrib/admindocs/views.py:60 +#: contrib/admindocs/views.py:62 msgid "tag:" msgstr "etiqueta:" -#: contrib/admindocs/views.py:90 contrib/admindocs/views.py:92 -#: contrib/admindocs/views.py:94 +#: contrib/admindocs/views.py:91 contrib/admindocs/views.py:93 +#: contrib/admindocs/views.py:95 msgid "filter:" msgstr "filtrar:" -#: contrib/admindocs/views.py:154 contrib/admindocs/views.py:156 -#: contrib/admindocs/views.py:158 +#: contrib/admindocs/views.py:155 contrib/admindocs/views.py:157 +#: contrib/admindocs/views.py:159 msgid "view:" msgstr "ver:" -#: contrib/admindocs/views.py:186 +#: contrib/admindocs/views.py:187 #, python-format msgid "App %r not found" msgstr "Aplicación %r no encontrada" -#: contrib/admindocs/views.py:193 +#: contrib/admindocs/views.py:194 #, python-format msgid "Model %(model_name)r not found in app %(app_label)r" msgstr "Modelo %(model_name)r no encontrado en aplicación %(app_label)r" -#: contrib/admindocs/views.py:205 +#: contrib/admindocs/views.py:206 #, python-format msgid "the related `%(app_label)s.%(data_type)s` object" msgstr "el objeto `%(app_label)s.%(data_type)s` relacionado" -#: contrib/admindocs/views.py:205 contrib/admindocs/views.py:227 -#: contrib/admindocs/views.py:241 contrib/admindocs/views.py:246 +#: contrib/admindocs/views.py:206 contrib/admindocs/views.py:228 +#: contrib/admindocs/views.py:242 contrib/admindocs/views.py:247 msgid "model:" msgstr "modelo:" -#: contrib/admindocs/views.py:236 +#: contrib/admindocs/views.py:237 #, python-format msgid "related `%(app_label)s.%(object_name)s` objects" msgstr "objetos `%(app_label)s.%(object_name)s` relacionados" -#: contrib/admindocs/views.py:241 +#: contrib/admindocs/views.py:242 #, python-format msgid "all %s" msgstr "todos los %s" -#: contrib/admindocs/views.py:246 +#: contrib/admindocs/views.py:247 #, python-format msgid "number of %s" msgstr "número de %s" -#: contrib/admindocs/views.py:251 +#: contrib/admindocs/views.py:252 #, python-format msgid "Fields on %s objects" msgstr "Campos en objetos %s" -#: contrib/admindocs/views.py:314 contrib/admindocs/views.py:325 -#: contrib/admindocs/views.py:327 contrib/admindocs/views.py:333 -#: contrib/admindocs/views.py:334 contrib/admindocs/views.py:336 +#: contrib/admindocs/views.py:315 contrib/admindocs/views.py:326 +#: contrib/admindocs/views.py:328 contrib/admindocs/views.py:334 +#: contrib/admindocs/views.py:335 contrib/admindocs/views.py:337 msgid "Integer" msgstr "Entero" -#: contrib/admindocs/views.py:315 +#: contrib/admindocs/views.py:316 msgid "Boolean (Either True or False)" msgstr "Booleano (Verdadero o Falso)" -#: contrib/admindocs/views.py:316 contrib/admindocs/views.py:335 +#: contrib/admindocs/views.py:317 contrib/admindocs/views.py:336 #, python-format msgid "String (up to %(max_length)s)" msgstr "Cadena (máximo %(max_length)s)" -#: contrib/admindocs/views.py:317 +#: contrib/admindocs/views.py:318 msgid "Comma-separated integers" msgstr "Enteros separados por comas" -#: contrib/admindocs/views.py:318 +#: contrib/admindocs/views.py:319 msgid "Date (without time)" msgstr "Fecha (sin hora)" -#: contrib/admindocs/views.py:319 +#: contrib/admindocs/views.py:320 msgid "Date (with time)" msgstr "Fecha (con hora)" -#: contrib/admindocs/views.py:320 +#: contrib/admindocs/views.py:321 msgid "Decimal number" msgstr "Número decimal" -#: contrib/admindocs/views.py:321 +#: contrib/admindocs/views.py:322 msgid "E-mail address" msgstr "Dirección de correo electrónico" -#: contrib/admindocs/views.py:322 contrib/admindocs/views.py:323 -#: contrib/admindocs/views.py:326 +#: contrib/admindocs/views.py:323 contrib/admindocs/views.py:324 +#: contrib/admindocs/views.py:327 msgid "File path" msgstr "Ruta de archivo" -#: contrib/admindocs/views.py:324 +#: contrib/admindocs/views.py:325 msgid "Floating point number" msgstr "Número de punto flotante" -#: contrib/admindocs/views.py:328 contrib/comments/models.py:58 +#: contrib/admindocs/views.py:329 contrib/comments/models.py:60 msgid "IP address" msgstr "Dirección IP" -#: contrib/admindocs/views.py:330 +#: contrib/admindocs/views.py:331 msgid "Boolean (Either True, False or None)" msgstr "Booleano (Verdadero, Falso o Nulo)" -#: contrib/admindocs/views.py:331 +#: contrib/admindocs/views.py:332 msgid "Relation to parent model" msgstr "Relación con el modelo padre" -#: contrib/admindocs/views.py:332 +#: contrib/admindocs/views.py:333 msgid "Phone number" msgstr "Número de teléfono" -#: contrib/admindocs/views.py:337 +#: contrib/admindocs/views.py:338 msgid "Text" msgstr "Texto" -#: contrib/admindocs/views.py:338 +#: contrib/admindocs/views.py:339 msgid "Time" msgstr "Hora" -#: contrib/admindocs/views.py:339 contrib/comments/forms.py:21 +#: contrib/admindocs/views.py:340 contrib/comments/forms.py:95 #: contrib/comments/templates/comments/moderation_queue.html:37 #: contrib/flatpages/admin.py:8 contrib/flatpages/models.py:7 msgid "URL" msgstr "URL" -#: contrib/admindocs/views.py:340 +#: contrib/admindocs/views.py:341 msgid "U.S. state (two uppercase letters)" msgstr "Estado de los EE.UU. (dos letras mayúsculas)" -#: contrib/admindocs/views.py:341 +#: contrib/admindocs/views.py:342 msgid "XML text" msgstr "Texto XML" -#: contrib/admindocs/views.py:367 +#: contrib/admindocs/views.py:368 #, python-format msgid "%s does not appear to be a urlpattern object" msgstr "%s no parece ser un objeto urlpattern" @@ -1174,21 +1239,21 @@ msgstr "Fechas importantes" msgid "Groups" msgstr "Grupos" -#: contrib/auth/admin.py:64 +#: contrib/auth/admin.py:80 msgid "Add user" msgstr "Agregar usuario" -#: contrib/auth/admin.py:90 +#: contrib/auth/admin.py:106 msgid "Password changed successfully." msgstr "Cambio de contraseña exitoso" -#: contrib/auth/admin.py:96 +#: contrib/auth/admin.py:112 #, python-format msgid "Change password: %s" msgstr "Cambiar contraseña: %s" #: contrib/auth/forms.py:15 contrib/auth/forms.py:48 -#: contrib/auth/models.py:127 +#: contrib/auth/models.py:128 msgid "" "Required. 30 characters or fewer. Alphanumeric characters only (letters, " "digits and underscores)." @@ -1208,8 +1273,8 @@ msgstr "Confirmación de contraseña" msgid "A user with that username already exists." msgstr "Ya existe un usuario con ese nombre." -#: contrib/auth/forms.py:36 contrib/auth/forms.py:154 -#: contrib/auth/forms.py:196 +#: contrib/auth/forms.py:36 contrib/auth/forms.py:155 +#: contrib/auth/forms.py:197 msgid "The two password fields didn't match." msgstr "Los dos campos de contraseñas no coinciden entre si." @@ -1237,24 +1302,24 @@ msgstr "" "Esa dirección de e-mail no está asociada a ninguna cuenta de usuario. ¿Está " "seguro de que ya se ha registrado?" -#: contrib/auth/forms.py:134 +#: contrib/auth/forms.py:135 #, python-format msgid "Password reset on %s" msgstr "Reinicialización de contraseña en %s" -#: contrib/auth/forms.py:142 +#: contrib/auth/forms.py:143 msgid "New password" msgstr "Contraseña nueva" -#: contrib/auth/forms.py:143 +#: contrib/auth/forms.py:144 msgid "New password confirmation" msgstr "Confirmación de contraseña nueva" -#: contrib/auth/forms.py:168 +#: contrib/auth/forms.py:169 msgid "Old password" msgstr "Contraseña antigua" -#: contrib/auth/forms.py:176 +#: contrib/auth/forms.py:177 msgid "Your old password was entered incorrectly. Please enter it again." msgstr "" "La antigua contraseña introducida es incorrecta. Por favor introdúzcala " @@ -1280,31 +1345,31 @@ msgstr "permisos" msgid "group" msgstr "grupo" -#: contrib/auth/models.py:91 contrib/auth/models.py:137 +#: contrib/auth/models.py:91 contrib/auth/models.py:138 msgid "groups" msgstr "grupos" -#: contrib/auth/models.py:127 +#: contrib/auth/models.py:128 msgid "username" msgstr "nombre de usuario" -#: contrib/auth/models.py:128 +#: contrib/auth/models.py:129 msgid "first name" msgstr "nombre" -#: contrib/auth/models.py:129 +#: contrib/auth/models.py:130 msgid "last name" msgstr "apellido" -#: contrib/auth/models.py:130 +#: contrib/auth/models.py:131 msgid "e-mail address" msgstr "dirección de correo electrónico" -#: contrib/auth/models.py:131 +#: contrib/auth/models.py:132 msgid "password" msgstr "contraseña" -#: contrib/auth/models.py:131 +#: contrib/auth/models.py:132 msgid "" "Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change " "password form</a>." @@ -1312,19 +1377,19 @@ msgstr "" "Use '[algo]$[salt]$[hexdigest]' o use el <a href=\"password/\">formulario de " "cambio de contraseña</a>." -#: contrib/auth/models.py:132 +#: contrib/auth/models.py:133 msgid "staff status" msgstr "es staff" -#: contrib/auth/models.py:132 +#: contrib/auth/models.py:133 msgid "Designates whether the user can log into this admin site." msgstr "Indica si el usuario puede ingresar a este sitio de administración." -#: contrib/auth/models.py:133 +#: contrib/auth/models.py:134 msgid "active" msgstr "activo" -#: contrib/auth/models.py:133 +#: contrib/auth/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -1332,11 +1397,11 @@ msgstr "" "Indica si el usuario debe ser tratado como un usuario activo. Desactive este " "campo en lugar de eliminar usuarios." -#: contrib/auth/models.py:134 +#: contrib/auth/models.py:135 msgid "superuser status" msgstr "es superusuario" -#: contrib/auth/models.py:134 +#: contrib/auth/models.py:135 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -1344,15 +1409,15 @@ msgstr "" "Indica que este usuario posee todos los permisos sin que sea necesario " "asignarle los mismos en forma explícita." -#: contrib/auth/models.py:135 +#: contrib/auth/models.py:136 msgid "last login" msgstr "último ingreso" -#: contrib/auth/models.py:136 +#: contrib/auth/models.py:137 msgid "date joined" msgstr "fecha de creación" -#: contrib/auth/models.py:138 +#: contrib/auth/models.py:139 msgid "" "In addition to the permissions manually assigned, this user will also get " "all permissions granted to each group he/she is in." @@ -1360,27 +1425,28 @@ msgstr "" "Además de los permisos asignados manualmente, este usuario también poseerá " "todos los permisos de los grupos a los que pertenezca." -#: contrib/auth/models.py:139 +#: contrib/auth/models.py:140 msgid "user permissions" msgstr "permisos de usuario" -#: contrib/auth/models.py:143 +#: contrib/auth/models.py:144 contrib/comments/models.py:50 +#: contrib/comments/models.py:168 msgid "user" msgstr "usuario" -#: contrib/auth/models.py:144 +#: contrib/auth/models.py:145 msgid "users" msgstr "usuarios" -#: contrib/auth/models.py:300 +#: contrib/auth/models.py:301 msgid "message" msgstr "mensaje" -#: contrib/auth/views.py:50 +#: contrib/auth/views.py:56 msgid "Logged out" msgstr "Sesión cerrada" -#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:429 msgid "Enter a valid e-mail address." msgstr "Introduzca una dirección de correo electrónico válida" @@ -1392,71 +1458,86 @@ msgstr "Contenido" msgid "Metadata" msgstr "Metadatos" -#: contrib/comments/forms.py:19 +#: contrib/comments/feeds.py:13 +#, python-format +msgid "%(site_name)s comments" +msgstr "comentarios en %(site_name)s" + +#: contrib/comments/feeds.py:23 +#, python-format +msgid "Latest comments on %(site_name)s" +msgstr "Últimos comentarios en %(site_name)s." + +#: contrib/comments/forms.py:93 #: contrib/comments/templates/comments/moderation_queue.html:34 msgid "Name" msgstr "Nombre" -#: contrib/comments/forms.py:20 +#: contrib/comments/forms.py:94 msgid "Email address" msgstr "Dirección de correo electrónico" -#: contrib/comments/forms.py:22 +#: contrib/comments/forms.py:96 #: contrib/comments/templates/comments/moderation_queue.html:35 msgid "Comment" msgstr "Comentario" -#: contrib/comments/forms.py:25 -msgid "" -"If you enter anything in this field your comment will be treated as spam" -msgstr "Si introduce algo en este campo su comentario será tratado como spam" - -#: contrib/comments/forms.py:125 +#: contrib/comments/forms.py:173 #, 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] "¡Controla tu lenguaje! Aquí no admitimos la palabra %s." msgstr[1] "¡Controla tu lenguaje! Aquí no admitimos las palabras %s." -#: contrib/comments/models.py:23 +#: contrib/comments/forms.py:180 +msgid "" +"If you enter anything in this field your comment will be treated as spam" +msgstr "Si introduce algo en este campo su comentario será tratado como spam" + +#: contrib/comments/models.py:22 contrib/contenttypes/models.py:74 +msgid "content type" +msgstr "tipo de contenido" + +#: contrib/comments/models.py:24 msgid "object ID" msgstr "ID de objeto" -#: contrib/comments/models.py:50 +#: contrib/comments/models.py:52 msgid "user's name" msgstr "nombre de usuario" -#: contrib/comments/models.py:51 +#: contrib/comments/models.py:53 msgid "user's email address" msgstr "dirección de correo electrónico del usuario" -#: contrib/comments/models.py:52 +#: contrib/comments/models.py:54 msgid "user's URL" msgstr "URL del usuario" -#: contrib/comments/models.py:54 +#: contrib/comments/models.py:56 contrib/comments/models.py:76 +#: contrib/comments/models.py:169 msgid "comment" msgstr "comentario" -#: contrib/comments/models.py:57 +#: contrib/comments/models.py:59 msgid "date/time submitted" msgstr "fecha/hora de envío" -#: contrib/comments/models.py:59 +#: contrib/comments/models.py:61 msgid "is public" msgstr "es público" -#: contrib/comments/models.py:60 +#: contrib/comments/models.py:62 msgid "" "Uncheck this box to make the comment effectively disappear from the site." msgstr "" "deseleccione esta caja para lograr que el comentario desaparezca del sitio." -#: contrib/comments/models.py:62 +#: contrib/comments/models.py:64 msgid "is removed" msgstr "se ha eliminado" -#: contrib/comments/models.py:63 +#: contrib/comments/models.py:65 msgid "" "Check this box if the comment is inappropriate. A \"This comment has been " "removed\" message will be displayed instead." @@ -1464,7 +1545,11 @@ msgstr "" "Marque esta caja si el comentario es inapropiado. En su lugar se mostrará un " "mensaje \"Este comentario ha sido eliminado\"." -#: contrib/comments/models.py:115 +#: contrib/comments/models.py:77 +msgid "comments" +msgstr "comentarios" + +#: contrib/comments/models.py:119 msgid "" "This comment was posted by an authenticated user and thus the name is read-" "only." @@ -1472,7 +1557,7 @@ msgstr "" "Este comentario ha sido enviado por un usuario identificado, por lo tanto el " "nombre no puede modificarse.%(text)s" -#: contrib/comments/models.py:124 +#: contrib/comments/models.py:128 msgid "" "This comment was posted by an authenticated user and thus the email is read-" "only." @@ -1480,7 +1565,7 @@ msgstr "" "Este comentario ha sido enviado por un usuario identificado, por lo tanto la " "dirección de correo electrónico no puede modificarse.%(text)s" -#: contrib/comments/models.py:149 +#: contrib/comments/models.py:153 #, python-format msgid "" "Posted by %(user)s at %(date)s\n" @@ -1495,6 +1580,22 @@ msgstr "" "\n" "http://%(domain)s%(url)s" +#: contrib/comments/models.py:170 +msgid "flag" +msgstr "marca" + +#: contrib/comments/models.py:171 +msgid "date" +msgstr "fecha" + +#: contrib/comments/models.py:181 +msgid "comment flag" +msgstr "marca de comentario" + +#: contrib/comments/models.py:182 +msgid "comment flags" +msgstr "marcas de comentario" + #: contrib/comments/templates/comments/approve.html:4 msgid "Approve a comment" msgstr "Aprobar un comentario" @@ -1554,13 +1655,13 @@ msgstr "Marcar" msgid "Thanks for flagging" msgstr "¡Gracias por marcar!" -#: contrib/comments/templates/comments/form.html:16 -#: contrib/comments/templates/comments/preview.html:31 +#: contrib/comments/templates/comments/form.html:17 +#: contrib/comments/templates/comments/preview.html:32 msgid "Post" msgstr "Remitir" -#: contrib/comments/templates/comments/form.html:17 -#: contrib/comments/templates/comments/preview.html:32 +#: contrib/comments/templates/comments/form.html:18 +#: contrib/comments/templates/comments/preview.html:33 msgid "Preview" msgstr "Previsualización" @@ -1606,33 +1707,29 @@ msgid "Thank you for your comment" msgstr "Gracias por dejar su comentario" #: contrib/comments/templates/comments/preview.html:4 -#: contrib/comments/templates/comments/preview.html:12 +#: contrib/comments/templates/comments/preview.html:13 msgid "Preview your comment" msgstr "Ver una copia previa de su comentario" -#: contrib/comments/templates/comments/preview.html:10 +#: contrib/comments/templates/comments/preview.html:11 msgid "Please correct the error below" msgid_plural "Please correct the errors below" msgstr[0] "Por favor, corrija el siguiente error." msgstr[1] "Por favor, corrija los siguientes errores." -#: contrib/comments/templates/comments/preview.html:15 +#: contrib/comments/templates/comments/preview.html:16 msgid "Post your comment" msgstr "Enviar su comentario" -#: contrib/comments/templates/comments/preview.html:15 +#: contrib/comments/templates/comments/preview.html:16 msgid "or make changes" msgstr "o realice modificaciones" -#: contrib/contenttypes/models.py:67 +#: contrib/contenttypes/models.py:70 msgid "python model class name" msgstr "nombre de la clase python del modelo" -#: contrib/contenttypes/models.py:71 -msgid "content type" -msgstr "tipo de contenido" - -#: contrib/contenttypes/models.py:72 +#: contrib/contenttypes/models.py:75 msgid "content types" msgstr "tipos de contenido" @@ -1703,18 +1800,26 @@ msgstr "" "Lamentablemente su formulario ha caducado. Por favor continúe rellenando el " "formulario en esta página." -#: contrib/gis/forms/fields.py:14 +#: contrib/gis/forms/fields.py:17 msgid "No geometry value provided." msgstr "No se ha proporcionado un valor de geometría." -#: contrib/gis/forms/fields.py:15 +#: contrib/gis/forms/fields.py:18 msgid "Invalid geometry value." msgstr "Valor de geometría no válido." -#: contrib/gis/forms/fields.py:16 +#: contrib/gis/forms/fields.py:19 msgid "Invalid geometry type." msgstr "Tipo de geometría no válido." +#: contrib/gis/forms/fields.py:20 +msgid "" +"An error occurred when transforming the geometry to the SRID of the geometry " +"form field." +msgstr "" +"Ha ocurrido un error mientras se transformaba la geometría al SRID del campo " +"de formulario de la misma." + #: contrib/humanize/templatetags/humanize.py:19 msgid "th" msgstr "th" @@ -2037,6 +2142,83 @@ msgstr "Introduzca un RUT chileno válido. EL formato es XX.XXX.XXX-X." msgid "The Chilean RUT is not valid." msgstr "El RUT chileno no es válido." +#: contrib/localflavor/cz/cz_regions.py:8 +msgid "Prague" +msgstr "Praga" + +#: contrib/localflavor/cz/cz_regions.py:9 +msgid "Central Bohemian Region" +msgstr "región Bohemia Central" + +#: contrib/localflavor/cz/cz_regions.py:10 +msgid "South Bohemian Region" +msgstr "región Bohemian Meridional" + +#: contrib/localflavor/cz/cz_regions.py:11 +msgid "Pilsen Region" +msgstr "región Pilsen" + +#: contrib/localflavor/cz/cz_regions.py:12 +msgid "Carlsbad Region" +msgstr "región Karlovy Vary" + +#: contrib/localflavor/cz/cz_regions.py:13 +msgid "Usti Region" +msgstr "región Ústí nad Labem" + +#: contrib/localflavor/cz/cz_regions.py:14 +msgid "Liberec Region" +msgstr "región Liberec" + +#: contrib/localflavor/cz/cz_regions.py:15 +msgid "Hradec Region" +msgstr "región Hradec Králové" + +#: contrib/localflavor/cz/cz_regions.py:16 +msgid "Pardubice Region" +msgstr "región Pardubice" + +#: contrib/localflavor/cz/cz_regions.py:17 +msgid "Vysocina Region" +msgstr "región Vysočina" + +#: contrib/localflavor/cz/cz_regions.py:18 +msgid "South Moravian Region" +msgstr "región Moravia Meridional" + +#: contrib/localflavor/cz/cz_regions.py:19 +msgid "Olomouc Region" +msgstr "región Olomouc" + +#: contrib/localflavor/cz/cz_regions.py:20 +msgid "Zlin Region" +msgstr "región Zlín" + +#: contrib/localflavor/cz/cz_regions.py:21 +msgid "Moravian-Silesian Region" +msgstr "región Moravia-Silesia" + +#: contrib/localflavor/cz/forms.py:27 contrib/localflavor/sk/forms.py:30 +msgid "Enter a postal code in the format XXXXX or XXX XX." +msgstr "Introduzca un código postal en formato XXXXX o XXX XX." + +#: contrib/localflavor/cz/forms.py:47 +msgid "Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX." +msgstr "" +"Introduzca un número de nacimiento en formato XXXXXX/XXXX o XXXXXXXXXX." + +#: contrib/localflavor/cz/forms.py:48 +msgid "Invalid optional parameter Gender, valid values are 'f' and 'm'" +msgstr "Parámetro opcional Género inválido, valores válidos son 'f' y 'm'" + +#: contrib/localflavor/cz/forms.py:49 +msgid "Enter a valid birth number." +msgstr "Introduzca un número ide nacimiento válido." + +#: contrib/localflavor/cz/forms.py:106 +msgid "Enter a valid IC number." +msgstr "Introduzca un número IC válido." + #: contrib/localflavor/de/de_states.py:5 msgid "Baden-Wuerttemberg" msgstr "Baden-Wuerttemberg" @@ -2868,19 +3050,19 @@ msgstr "" msgid "Wrong checksum for the Tax Number (NIP)." msgstr "Código de verificación de Número Impostitivo (NIP) inválido." -#: contrib/localflavor/pl/forms.py:111 -msgid "National Business Register Number (REGON) consists of 7 or 9 digits." +#: contrib/localflavor/pl/forms.py:109 +msgid "National Business Register Number (REGON) consists of 9 or 14 digits." msgstr "" -"Los Números Nacionales de Registro de Negocios (REGON) constan de 7 o 9 " +"Los Números Nacionales de Registro de Negocios (REGON) constan de 9 o 14 " "dígitos." -#: contrib/localflavor/pl/forms.py:112 +#: contrib/localflavor/pl/forms.py:110 msgid "Wrong checksum for the National Business Register Number (REGON)." msgstr "" "Código de verificación de Número Nacional de Registro de negocios (REGON) " "inválido." -#: contrib/localflavor/pl/forms.py:155 +#: contrib/localflavor/pl/forms.py:148 msgid "Enter a postal code in the format XX-XXX." msgstr "Introduzca un código postal en formato XX-XXX." @@ -2968,10 +3150,6 @@ msgstr "Los números telefónicos deben respetar el formato XXXX-XXXXXX." msgid "Enter a valid postal code in the format XXXXXX" msgstr "Introduzca un código postal válido en formato XXXXXX" -#: contrib/localflavor/sk/forms.py:30 -msgid "Enter a postal code in the format XXXXX or XXX XX." -msgstr "Introduzca un código postal en formato XXXXX o XXX XX." - #: contrib/localflavor/sk/sk_districts.py:8 msgid "Banska Bystrica" msgstr "Banska Bystrica" @@ -3290,35 +3468,35 @@ msgstr "Zilina" #: contrib/localflavor/sk/sk_regions.py:8 msgid "Banska Bystrica region" -msgstr "region Banska Bystrica" +msgstr "región Banska Bystrica" #: contrib/localflavor/sk/sk_regions.py:9 msgid "Bratislava region" -msgstr "region Bratislava" +msgstr "región Bratislava" #: contrib/localflavor/sk/sk_regions.py:10 msgid "Kosice region" -msgstr "region Kosice" +msgstr "región Kosice" #: contrib/localflavor/sk/sk_regions.py:11 msgid "Nitra region" -msgstr "region Nitra" +msgstr "región Nitra" #: contrib/localflavor/sk/sk_regions.py:12 msgid "Presov region" -msgstr "region Presov" +msgstr "región Presov" #: contrib/localflavor/sk/sk_regions.py:13 msgid "Trencin region" -msgstr "region Trencin" +msgstr "región Trencin" #: contrib/localflavor/sk/sk_regions.py:14 msgid "Trnava region" -msgstr "region Trnava" +msgstr "región Trnava" #: contrib/localflavor/sk/sk_regions.py:15 msgid "Zilina region" -msgstr "region Zilina" +msgstr "región Zilina" #: contrib/localflavor/uk/forms.py:21 msgid "Enter a valid postcode." @@ -3720,57 +3898,61 @@ msgstr "nombre para visualizar" msgid "sites" msgstr "sitios" -#: db/models/fields/__init__.py:348 db/models/fields/__init__.py:683 +#: db/models/fields/__init__.py:356 db/models/fields/__init__.py:710 msgid "This value must be an integer." msgstr "Este valor debe ser un número entero." -#: db/models/fields/__init__.py:379 +#: db/models/fields/__init__.py:388 msgid "This value must be either True or False." msgstr "Este valor debe ser True o False." -#: db/models/fields/__init__.py:412 +#: db/models/fields/__init__.py:427 msgid "This field cannot be null." msgstr "Este campo no puede ser nulo." -#: db/models/fields/__init__.py:428 +#: db/models/fields/__init__.py:443 msgid "Enter only digits separated by commas." msgstr "Introduzca sólo dígitos separados por comas." -#: db/models/fields/__init__.py:459 +#: db/models/fields/__init__.py:474 msgid "Enter a valid date in YYYY-MM-DD format." msgstr "Introduzca una fecha válida en formato AAAA-MM-DD." -#: db/models/fields/__init__.py:468 +#: db/models/fields/__init__.py:483 #, python-format msgid "Invalid date: %s" msgstr "Fecha no válida: %s" -#: db/models/fields/__init__.py:532 db/models/fields/__init__.py:550 +#: db/models/fields/__init__.py:547 db/models/fields/__init__.py:565 msgid "Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format." msgstr "" "Introduzca un valor de fecha/hora válido en formato AAAA-MM-DD HH:MM[:ss[." "uuuuuu]]." -#: db/models/fields/__init__.py:586 +#: db/models/fields/__init__.py:601 msgid "This value must be a decimal number." msgstr "Este valor debe ser un número decimal." -#: db/models/fields/__init__.py:719 +#: db/models/fields/__init__.py:686 +msgid "This value must be a float." +msgstr "Este valor debe ser un valor en representación de punto flotante." + +#: db/models/fields/__init__.py:746 msgid "This value must be either None, True or False." msgstr "Este valor debe ser None, True o False." -#: db/models/fields/__init__.py:817 db/models/fields/__init__.py:831 +#: db/models/fields/__init__.py:849 db/models/fields/__init__.py:863 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Introduzca un valor de hora válido en formato HH:MM[:ss[.uuuuuu]]." -#: db/models/fields/related.py:761 +#: db/models/fields/related.py:792 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" "Mantenga presionada \"Control\" (\"Command\" en una Mac) para seleccionar " "más de uno." -#: db/models/fields/related.py:838 +#: db/models/fields/related.py:870 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" @@ -3837,32 +4019,40 @@ msgstr "Asegúrese de que no existan mas de %s lugares decimales." msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Asegúrese de que no existan mas de %s dígitos antes del punto decimal." -#: forms/fields.py:287 forms/fields.py:849 +#: forms/fields.py:288 forms/fields.py:863 msgid "Enter a valid date." msgstr "Introduzca una fecha válida." -#: forms/fields.py:321 forms/fields.py:850 +#: forms/fields.py:322 forms/fields.py:864 msgid "Enter a valid time." msgstr "Introduzca un valor de hora válido." -#: forms/fields.py:360 +#: forms/fields.py:361 msgid "Enter a valid date/time." msgstr "Introduzca un valor de fecha/hora válido." -#: forms/fields.py:446 +#: forms/fields.py:447 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "No se envió un archivo. Verifique el tipo de codificación en el formulario." -#: forms/fields.py:447 +#: forms/fields.py:448 msgid "No file was submitted." msgstr "No se envió ningún archivo." -#: forms/fields.py:448 +#: forms/fields.py:449 msgid "The submitted file is empty." msgstr "El archivo enviado está vacío." -#: forms/fields.py:477 +#: forms/fields.py:450 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "" +"Asegúrese de que este nombre de archivo tenga como máximo %(max)d caracteres " +"(tiene %(length)d)." + +#: forms/fields.py:483 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -3870,107 +4060,141 @@ msgstr "" "Envíe una imagen válida. El archivo que ha enviado no era una imagen o se " "trataba de una imagen corrupta." -#: forms/fields.py:538 +#: forms/fields.py:544 msgid "Enter a valid URL." msgstr "Introduzca una URL válida." -#: forms/fields.py:539 +#: forms/fields.py:545 msgid "This URL appears to be a broken link." msgstr "La URL parece ser un enlace roto." -#: forms/fields.py:618 forms/fields.py:696 +#: forms/fields.py:625 forms/fields.py:703 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Seleccione una opción válida. %(value)s no es una de las opciones " "disponibles." -#: forms/fields.py:697 forms/fields.py:758 forms/models.py:720 +#: forms/fields.py:704 forms/fields.py:765 forms/models.py:991 msgid "Enter a list of values." msgstr "Introduzca una lista de valores." -#: forms/fields.py:878 +#: forms/fields.py:892 msgid "Enter a valid IPv4 address." msgstr "Introduzca una dirección IPv4 válida" -#: forms/fields.py:888 +#: forms/fields.py:902 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "Introduzca un 'slug' válido consistente en letras, números o guiones." -#: forms/formsets.py:242 forms/formsets.py:244 +#: forms/formsets.py:271 forms/formsets.py:273 msgid "Order" msgstr "Ordenar" -#: forms/models.py:281 forms/models.py:290 +#: forms/models.py:367 +#, python-format +msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." +msgstr "" +"%(field_name)s debe ser único/a para un %(lookup)s %(date_field)s " +"determinado." + +#: forms/models.py:381 forms/models.py:389 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "Ya existe un/a %(model_name)s con este/a %(field_label)s." -#: forms/models.py:587 +#: forms/models.py:581 +#, fuzzy, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "Por favor, corrija el siguiente error." + +#: forms/models.py:585 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: forms/models.py:591 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: forms/models.py:599 +#, fuzzy +msgid "Please correct the duplicate values below." +msgstr "Por favor, corrija el siguiente error." + +#: forms/models.py:855 msgid "The inline foreign key did not match the parent instance primary key." msgstr "" "La clave foránea del modelo inline no coincide con la de la instancia padre." -#: forms/models.py:650 +#: forms/models.py:918 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "" "Seleccione una opción válida. Esa opción no es una de las opciones " "disponibles." -#: forms/models.py:721 +#: forms/models.py:992 #, python-format msgid "Select a valid choice. %s is not one of the available choices." msgstr "" "Seleccione una opción válida. %s no es una de las opciones disponibles." -#: template/defaultfilters.py:741 +#: forms/models.py:994 +#, python-format +msgid "\"%s\" is not a valid value for a primary key." +msgstr "\"%s\" no es un valor válido para una clave primaria." + +#: template/defaultfilters.py:767 msgid "yes,no,maybe" msgstr "si,no,talvez" -#: template/defaultfilters.py:772 +#: template/defaultfilters.py:798 #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" msgstr[0] "%(size)d byte" msgstr[1] "%(size)d bytes" -#: template/defaultfilters.py:774 +#: template/defaultfilters.py:800 #, python-format msgid "%.1f KB" msgstr "%.1f KB" -#: template/defaultfilters.py:776 +#: template/defaultfilters.py:802 #, python-format msgid "%.1f MB" msgstr "%.1f MB" -#: template/defaultfilters.py:777 +#: template/defaultfilters.py:803 #, python-format msgid "%.1f GB" msgstr "%.1f GB" -#: utils/dateformat.py:41 +#: utils/dateformat.py:42 msgid "p.m." msgstr "p.m." -#: utils/dateformat.py:42 +#: utils/dateformat.py:43 msgid "a.m." msgstr "a.m." -#: utils/dateformat.py:47 +#: utils/dateformat.py:48 msgid "PM" msgstr "PM" -#: utils/dateformat.py:48 +#: utils/dateformat.py:49 msgid "AM" msgstr "AM" -#: utils/dateformat.py:97 +#: utils/dateformat.py:98 msgid "midnight" msgstr "medianoche" -#: utils/dateformat.py:99 +#: utils/dateformat.py:100 msgid "noon" msgstr "mediodía" @@ -4194,33 +4418,33 @@ msgid_plural "minutes" msgstr[0] "minuto" msgstr[1] "minutos" -#: utils/timesince.py:43 +#: utils/timesince.py:45 msgid "minutes" msgstr "minutos" -#: utils/timesince.py:48 +#: utils/timesince.py:50 #, python-format msgid "%(number)d %(type)s" msgstr "%(number)d %(type)s" -#: utils/timesince.py:54 +#: utils/timesince.py:56 #, python-format msgid ", %(number)d %(type)s" msgstr ", %(number)d %(type)s" -#: utils/translation/trans_real.py:403 +#: utils/translation/trans_real.py:399 msgid "DATE_FORMAT" msgstr "j N Y" -#: utils/translation/trans_real.py:405 +#: utils/translation/trans_real.py:401 msgid "TIME_FORMAT" msgstr "P" -#: utils/translation/trans_real.py:421 +#: utils/translation/trans_real.py:417 msgid "YEAR_MONTH_FORMAT" msgstr "F Y" -#: utils/translation/trans_real.py:422 +#: utils/translation/trans_real.py:418 msgid "MONTH_DAY_FORMAT" msgstr "j \\de F" diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html index aaba97fdb7..358813290c 100644 --- a/django/contrib/admin/templates/admin/pagination.html +++ b/django/contrib/admin/templates/admin/pagination.html @@ -8,5 +8,5 @@ {% endif %} {{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endifequal %} {% if show_all_url %} <a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %} -{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="Save"/>{% endif %} +{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% trans 'Save' %}"/>{% endif %} </p> diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index 838889fbad..105cbfbec5 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -35,7 +35,7 @@ class GeoWhereNode(WhereNode): return super(WhereNode, self).add(data, connector) obj, lookup_type, value = data - alias, col, field = obj.alias, obj.col, obj.field + col, field = obj.col, obj.field if not hasattr(field, "geom_type"): # Not a geographic field, so call `WhereNode.add`. @@ -76,7 +76,7 @@ class GeoWhereNode(WhereNode): # the `get_geo_where_clause` to construct the appropriate # spatial SQL when `make_atom` is called. annotation = GeoAnnotation(field, value, where) - return super(WhereNode, self).add(((alias, col, field.db_type()), lookup_type, annotation, params), connector) + return super(WhereNode, self).add(((obj.alias, col, field.db_type()), lookup_type, annotation, params), connector) def make_atom(self, child, qn): obj, lookup_type, value_annot, params = child diff --git a/django/contrib/gis/maps/google/gmap.py b/django/contrib/gis/maps/google/gmap.py index dd4f67bc52..de8f75c5a0 100644 --- a/django/contrib/gis/maps/google/gmap.py +++ b/django/contrib/gis/maps/google/gmap.py @@ -143,7 +143,7 @@ class GoogleMap(object): @property def icons(self): "Returns a sequence of GIcon objects in this map." - return [marker.icon for marker in self.markers if marker.icon] + return set([marker.icon for marker in self.markers if marker.icon]) class GoogleMapSet(GoogleMap): @@ -221,6 +221,6 @@ class GoogleMapSet(GoogleMap): @property def icons(self): "Returns a sequence of all icons in each map of the set." - icons = [] - for map in self.maps: icons.extend(map.icons) + icons = set() + for map in self.maps: icons |= map.icons return icons diff --git a/django/contrib/gis/maps/google/overlays.py b/django/contrib/gis/maps/google/overlays.py index 4f1247d0a4..c2ebb3c992 100644 --- a/django/contrib/gis/maps/google/overlays.py +++ b/django/contrib/gis/maps/google/overlays.py @@ -231,6 +231,14 @@ class GIcon(object): self.iconanchor = iconanchor self.infowindowanchor = infowindowanchor + def __cmp__(self, other): + return cmp(self.varname, other.varname) + + def __hash__(self): + # XOR with hash of GIcon type so that hash('varname') won't + # equal hash(GIcon('varname')). + return hash(self.__class__) ^ hash(self.varname) + class GMarker(GOverlayBase): """ A Python wrapper for the Google GMarker object. For more information diff --git a/django/contrib/gis/tests/relatedapp/models.py b/django/contrib/gis/tests/relatedapp/models.py index d7dd6bbfd2..1125d7fb85 100644 --- a/django/contrib/gis/tests/relatedapp/models.py +++ b/django/contrib/gis/tests/relatedapp/models.py @@ -32,3 +32,13 @@ class Parcel(models.Model): border2 = models.PolygonField(srid=2276) objects = models.GeoManager() def __unicode__(self): return self.name + +# These use the GeoManager but do not have any geographic fields. +class Author(models.Model): + name = models.CharField(max_length=100) + objects = models.GeoManager() + +class Book(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author, related_name='books') + objects = models.GeoManager() diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 77f6c73bb6..8c4f83b15a 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,10 +1,10 @@ import os, unittest from django.contrib.gis.geos import * from django.contrib.gis.db.backend import SpatialBackend -from django.contrib.gis.db.models import F, Extent, Union +from django.contrib.gis.db.models import Count, Extent, F, Union from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite from django.conf import settings -from models import City, Location, DirectoryEntry, Parcel +from models import City, Location, DirectoryEntry, Parcel, Book, Author cities = (('Aurora', 'TX', -97.516111, 33.058333), ('Roswell', 'NM', -104.528056, 33.387222), @@ -196,8 +196,8 @@ class RelatedGeoModelTest(unittest.TestCase): # ID values do not match their City ID values. loc1 = Location.objects.create(point='POINT (-95.363151 29.763374)') loc2 = Location.objects.create(point='POINT (-96.801611 32.782057)') - dallas = City.objects.create(name='Dallas', location=loc2) - houston = City.objects.create(name='Houston', location=loc1) + dallas = City.objects.create(name='Dallas', state='TX', location=loc2) + houston = City.objects.create(name='Houston', state='TX', location=loc1) # The expected ID values -- notice the last two location IDs # are out of order. We want to make sure that the related @@ -231,6 +231,32 @@ class RelatedGeoModelTest(unittest.TestCase): q = pickle.loads(q_str) self.assertEqual(GeoQuery, q.__class__) + def test12_count(self): + "Testing `Count` aggregate use with the `GeoManager`. See #11087." + # Creating a new City, 'Fort Worth', that uses the same location + # as Dallas. + dallas = City.objects.get(name='Dallas') + ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location) + + # Count annotation should be 2 for the Dallas location now. + loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id) + self.assertEqual(2, loc.num_cities) + + # Creating some data for the Book/Author non-geo models that + # use GeoManager. See #11087. + tp = Author.objects.create(name='Trevor Paglen') + Book.objects.create(title='Torture Taxi', author=tp) + Book.objects.create(title='I Could Tell You But Then You Would Have to be Destroyed by Me', author=tp) + Book.objects.create(title='Blank Spots on the Map', author=tp) + wp = Author.objects.create(name='William Patry') + Book.objects.create(title='Patry on Copyright', author=wp) + + # Should only be one author (Trevor Paglen) returned by this query, and + # the annotation should have 3 for the number of books. + qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1) + self.assertEqual(1, len(qs)) + self.assertEqual(3, qs[0].num_books) + # TODO: Related tests for KML, GML, and distance lookups. def suite(): diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py index c0c3c40a69..145bd221bb 100644 --- a/django/contrib/gis/utils/ogrinspect.py +++ b/django/contrib/gis/utils/ogrinspect.py @@ -12,12 +12,12 @@ from django.contrib.gis.gdal.field import OFTDate, OFTDateTime, OFTInteger, OFTR def mapping(data_source, geom_name='geom', layer_key=0, multi_geom=False): """ - Given a DataSource, generates a dictionary that may be used + Given a DataSource, generates a dictionary that may be used for invoking the LayerMapping utility. Keyword Arguments: `geom_name` => The name of the geometry field to use for the model. - + `layer_key` => The key for specifying which layer in the DataSource to use; defaults to 0 (the first layer). May be an integer index or a string identifier for the layer. @@ -31,7 +31,7 @@ def mapping(data_source, geom_name='geom', layer_key=0, multi_geom=False): pass else: raise TypeError('Data source parameter must be a string or a DataSource object.') - + # Creating the dictionary. _mapping = {} @@ -52,32 +52,32 @@ def ogrinspect(*args, **kwargs): model name this function will generate a GeoDjango model. Usage: - + >>> from django.contrib.gis.utils import ogrinspect >>> ogrinspect('/path/to/shapefile.shp','NewModel') - + ...will print model definition to stout - + or put this in a python script and use to redirect the output to a new model like: - + $ python generate_model.py > myapp/models.py - - # generate_model.py + + # generate_model.py from django.contrib.gis.utils import ogrinspect shp_file = 'data/mapping_hacks/world_borders.shp' model_name = 'WorldBorders' print ogrinspect(shp_file, model_name, multi_geom=True, srid=4326, geom_name='shapes', blank=True) - + Required Arguments `datasource` => string or DataSource object to file pointer - + `model name` => string of name of new model class to create - + Optional Keyword Arguments - `geom_name` => For specifying the model name for the Geometry Field. + `geom_name` => For specifying the model name for the Geometry Field. Otherwise will default to `geom` `layer_key` => The key for specifying which layer in the DataSource to use; @@ -86,24 +86,24 @@ def ogrinspect(*args, **kwargs): `srid` => The SRID to use for the Geometry Field. If it can be determined, the SRID of the datasource is used. - + `multi_geom` => Boolean (default: False) - specify as multigeometry. - + `name_field` => String - specifies a field name to return for the `__unicode__` function (which will be generated if specified). - - `imports` => Boolean (default: True) - set to False to omit the - `from django.contrib.gis.db import models` code from the + + `imports` => Boolean (default: True) - set to False to omit the + `from django.contrib.gis.db import models` code from the autogenerated models thus avoiding duplicated imports when building more than one model by batching ogrinspect() - + `decimal` => Boolean or sequence (default: False). When set to True all generated model fields corresponding to the `OFTReal` type will be `DecimalField` instead of `FloatField`. A sequence of specific field names to generate as `DecimalField` may also be used. `blank` => Boolean or sequence (default: False). When set to True all - generated model fields will have `blank=True`. If the user wants to + generated model fields will have `blank=True`. If the user wants to give specific fields to have blank, then a list/tuple of OGR field names may be used. @@ -111,13 +111,13 @@ def ogrinspect(*args, **kwargs): model fields will have `null=True`. If the user wants to specify give specific fields to have null, then a list/tuple of OGR field names may be used. - + Note: This routine calls the _ogrinspect() helper to do the heavy lifting. """ return '\n'.join(s for s in _ogrinspect(*args, **kwargs)) def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=None, - multi_geom=False, name_field=None, imports=True, + multi_geom=False, name_field=None, imports=True, decimal=False, blank=False, null=False): """ Helper routine for `ogrinspect` that generates GeoDjango models corresponding @@ -140,7 +140,7 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non # keyword arguments. def process_kwarg(kwarg): if isinstance(kwarg, (list, tuple)): - return [s.lower() for s in kwarg] + return [s.lower() for s in kwarg] elif kwarg: return [s.lower() for s in ogr_fields] else: @@ -164,18 +164,18 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non yield '' yield 'class %s(models.Model):' % model_name - + for field_name, width, precision, field_type in izip(ogr_fields, layer.field_widths, layer.field_precisions, layer.field_types): # The model field name. mfield = field_name.lower() if mfield[-1:] == '_': mfield += 'field' - + # Getting the keyword args string. kwargs_str = get_kwargs_str(field_name) if field_type is OFTReal: # By default OFTReals are mapped to `FloatField`, however, they - # may also be mapped to `DecimalField` if specified in the + # may also be mapped to `DecimalField` if specified in the # `decimal` keyword. if field_name.lower() in decimal_fields: yield ' %s = models.DecimalField(max_digits=%d, decimal_places=%d%s)' % (mfield, width, precision, kwargs_str) @@ -192,8 +192,8 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non elif field_type is OFTDate: yield ' %s = models.TimeField(%s)' % (mfield, kwargs_str[2:]) else: - raise TypeError('Unknown field type %s in %s' % (fld_type, mfield)) - + raise TypeError('Unknown field type %s in %s' % (field_type, mfield)) + # TODO: Autodetection of multigeometry types (see #7218). gtype = layer.geom_type if multi_geom and gtype.num in (1, 2, 3): diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 61a903e36c..aab4f3789f 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -142,13 +142,13 @@ class FileDescriptor(object): """ The descriptor for the file attribute on the model instance. Returns a FieldFile when accessed so you can do stuff like:: - + >>> instance.file.size - + Assigns a file object on assignment so you can do:: - + >>> instance.file = File(...) - + """ def __init__(self, field): self.field = field @@ -156,9 +156,9 @@ class FileDescriptor(object): def __get__(self, instance=None, owner=None): if instance is None: raise AttributeError( - "The '%s' attribute can only be accessed from %s instances." + "The '%s' attribute can only be accessed from %s instances." % (self.field.name, owner.__name__)) - + # This is slightly complicated, so worth an explanation. # instance.file`needs to ultimately return some instance of `File`, # probably a subclass. Additionally, this returned object needs to have @@ -168,8 +168,8 @@ class FileDescriptor(object): # peek below you can see that we're not. So depending on the current # value of the field we have to dynamically construct some sort of # "thing" to return. - - # The instance dict contains whatever was originally assigned + + # The instance dict contains whatever was originally assigned # in __set__. file = instance.__dict__[self.field.name] @@ -186,14 +186,14 @@ class FileDescriptor(object): # Other types of files may be assigned as well, but they need to have # the FieldFile interface added to the. Thus, we wrap any other type of - # File inside a FieldFile (well, the field's attr_class, which is + # File inside a FieldFile (well, the field's attr_class, which is # usually FieldFile). elif isinstance(file, File) and not isinstance(file, FieldFile): file_copy = self.field.attr_class(instance, self.field, file.name) file_copy.file = file file_copy._committed = False instance.__dict__[self.field.name] = file_copy - + # Finally, because of the (some would say boneheaded) way pickle works, # the underlying FieldFile might not actually itself have an associated # file. So we need to reset the details of the FieldFile in those cases. @@ -201,7 +201,7 @@ class FileDescriptor(object): file.instance = instance file.field = self.field file.storage = self.field.storage - + # That was fun, wasn't it? return instance.__dict__[self.field.name] @@ -212,7 +212,7 @@ class FileField(Field): # The class to wrap instance attributes in. Accessing the file object off # the instance will always return an instance of attr_class. attr_class = FieldFile - + # The descriptor to use for accessing the attribute off of the class. descriptor_class = FileDescriptor @@ -300,40 +300,20 @@ class ImageFileDescriptor(FileDescriptor): assigning the width/height to the width_field/height_field, if appropriate. """ def __set__(self, instance, value): + previous_file = instance.__dict__.get(self.field.name) super(ImageFileDescriptor, self).__set__(instance, value) - - # The rest of this method deals with width/height fields, so we can - # bail early if neither is used. - if not self.field.width_field and not self.field.height_field: - return - - # We need to call the descriptor's __get__ to coerce this assigned - # value into an instance of the right type (an ImageFieldFile, in this - # case). - value = self.__get__(instance) - - if not value: - return - - # Get the image dimensions, making sure to leave the file in the same - # state (opened or closed) that we got it in. However, we *don't* rewind - # the file pointer if the file is already open. This is in keeping with - # most Python standard library file operations that leave it up to the - # user code to reset file pointers after operations that move it. - from django.core.files.images import get_image_dimensions - close = value.closed - value.open() - try: - width, height = get_image_dimensions(value) - finally: - if close: - value.close() - - # Update the width and height fields - if self.field.width_field: - setattr(value.instance, self.field.width_field, width) - if self.field.height_field: - setattr(value.instance, self.field.height_field, height) + + # To prevent recalculating image dimensions when we are instantiating + # an object from the database (bug #11084), only update dimensions if + # the field had a value before this assignment. Since the default + # value for FileField subclasses is an instance of field.attr_class, + # previous_file will only be None when we are called from + # Model.__init__(). The ImageField.update_dimension_fields method + # hooked up to the post_init signal handles the Model.__init__() cases. + # Assignment happening outside of Model.__init__() will trigger the + # update right here. + if previous_file is not None: + self.field.update_dimension_fields(instance, force=True) class ImageFieldFile(ImageFile, FieldFile): def delete(self, save=True): @@ -350,6 +330,69 @@ class ImageField(FileField): self.width_field, self.height_field = width_field, height_field FileField.__init__(self, verbose_name, name, **kwargs) + def contribute_to_class(self, cls, name): + super(ImageField, self).contribute_to_class(cls, name) + # Attach update_dimension_fields so that dimension fields declared + # after their corresponding image field don't stay cleared by + # Model.__init__, see bug #11196. + signals.post_init.connect(self.update_dimension_fields, sender=cls) + + def update_dimension_fields(self, instance, force=False, *args, **kwargs): + """ + Updates field's width and height fields, if defined. + + This method is hooked up to model's post_init signal to update + dimensions after instantiating a model instance. However, dimensions + won't be updated if the dimensions fields are already populated. This + avoids unnecessary recalculation when loading an object from the + database. + + Dimensions can be forced to update with force=True, which is how + ImageFileDescriptor.__set__ calls this method. + """ + # Nothing to update if the field doesn't have have dimension fields. + has_dimension_fields = self.width_field or self.height_field + if not has_dimension_fields: + return + + # getattr will call the ImageFileDescriptor's __get__ method, which + # coerces the assigned value into an instance of self.attr_class + # (ImageFieldFile in this case). + file = getattr(instance, self.attname) + + # Nothing to update if we have no file and not being forced to update. + if not file and not force: + return + + dimension_fields_filled = not( + (self.width_field and not getattr(instance, self.width_field)) + or (self.height_field and not getattr(instance, self.height_field)) + ) + # When both dimension fields have values, we are most likely loading + # data from the database or updating an image field that already had + # an image stored. In the first case, we don't want to update the + # dimension fields because we are already getting their values from the + # database. In the second case, we do want to update the dimensions + # fields and will skip this return because force will be True since we + # were called from ImageFileDescriptor.__set__. + if dimension_fields_filled and not force: + return + + # file should be an instance of ImageFieldFile or should be None. + if file: + width = file.width + height = file.height + else: + # No file, so clear dimensions fields. + width = None + height = None + + # Update the width and height fields. + if self.width_field: + setattr(instance, self.width_field, width) + if self.height_field: + setattr(instance, self.height_field, height) + def formfield(self, **kwargs): defaults = {'form_class': forms.ImageField} defaults.update(kwargs) diff --git a/django/db/models/query.py b/django/db/models/query.py index 6a8d7d5e64..46a86fc03c 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -7,6 +7,8 @@ try: except NameError: from sets import Set as set # Python 2.3 fallback +from copy import deepcopy + from django.db import connection, transaction, IntegrityError from django.db.models.aggregates import Aggregate from django.db.models.fields import DateField @@ -40,6 +42,17 @@ class QuerySet(object): # PYTHON MAGIC METHODS # ######################## + def __deepcopy__(self, memo): + """ + Deep copy of a QuerySet doesn't populate the cache + """ + obj_dict = deepcopy(self.__dict__, memo) + obj_dict['_iter'] = None + + obj = self.__class__() + obj.__dict__.update(obj_dict) + return obj + def __getstate__(self): """ Allows the QuerySet to be pickled. @@ -190,7 +203,25 @@ class QuerySet(object): index_start = len(extra_select) aggregate_start = index_start + len(self.model._meta.fields) - load_fields = only_load.get(self.model) + load_fields = [] + # If only/defer clauses have been specified, + # build the list of fields that are to be loaded. + if only_load: + for field, model in self.model._meta.get_fields_with_model(): + if model is None: + model = self.model + if field == self.model._meta.pk: + # Record the index of the primary key when it is found + pk_idx = len(load_fields) + try: + if field.name in only_load[model]: + # Add a field that has been explicitly included + load_fields.append(field.name) + except KeyError: + # Model wasn't explicitly listed in the only_load table + # Therefore, we need to load all fields from this model + load_fields.append(field.name) + skip = None if load_fields and not fill_cache: # Some fields have been deferred, so we have to initialise @@ -355,10 +386,11 @@ class QuerySet(object): # Delete objects in chunks to prevent the list of related objects from # becoming too long. + seen_objs = None 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 = CollectedObjects() + seen_objs = CollectedObjects(seen_objs) for object in del_query[:CHUNK_SIZE]: object._collect_sub_objects(seen_objs) diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 7a5ad919a1..6a6b69013f 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -32,11 +32,21 @@ class CollectedObjects(object): This is used for the database object deletion routines so that we can calculate the 'leaf' objects which should be deleted first. + + previously_seen is an optional argument. It must be a CollectedObjects + instance itself; any previously_seen collected object will be blocked from + being added to this instance. """ - def __init__(self): + def __init__(self, previously_seen=None): self.data = {} self.children = {} + if previously_seen: + self.blocked = previously_seen.blocked + for cls, seen in previously_seen.data.items(): + self.blocked.setdefault(cls, SortedDict()).update(seen) + else: + self.blocked = {} def add(self, model, pk, obj, parent_model, nullable=False): """ @@ -53,6 +63,9 @@ class CollectedObjects(object): Returns True if the item already existed in the structure and False otherwise. """ + if pk in self.blocked.get(model, {}): + return True + d = self.data.setdefault(model, SortedDict()) retval = pk in d d[pk] = obj diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 394e30ba97..23f99e41ad 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -635,10 +635,10 @@ class BaseQuery(object): # models. workset = {} for model, values in seen.iteritems(): - for field, f_model in model._meta.get_fields_with_model(): + for field in model._meta.local_fields: if field in values: continue - add_to_dict(workset, f_model or model, field) + add_to_dict(workset, model, field) for model, values in must_include.iteritems(): # If we haven't included a model in workset, we don't add the # corresponding must_include fields for that model, since an @@ -657,6 +657,12 @@ class BaseQuery(object): # included any fields, we have to make sure it's mentioned # so that only the "must include" fields are pulled in. seen[model] = values + # Now ensure that every model in the inheritance chain is mentioned + # in the parent list. Again, it must be mentioned to ensure that + # only "must include" fields are pulled in. + for model in orig_opts.get_parent_list(): + if model not in seen: + seen[model] = set() for model, values in seen.iteritems(): callback(target, model, values) @@ -689,7 +695,7 @@ class BaseQuery(object): 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. + some cases to avoid ambiguity with nested queries. """ qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name @@ -1303,7 +1309,7 @@ class BaseQuery(object): opts = self.model._meta root_alias = self.tables[0] seen = {None: root_alias} - + # Skip all proxy to the root proxied model proxied_model = get_proxied_model(opts) @@ -1619,10 +1625,14 @@ class BaseQuery(object): entry.negate() self.where.add(entry, AND) break - elif not (lookup_type == 'in' and not value) and field.null: + elif not (lookup_type == 'in' + and not hasattr(value, 'as_sql') + and not hasattr(value, '_as_sql') + and not value) and field.null: # Leaky abstraction artifact: We have to specifically # exclude the "foo__in=[]" case from this handling, because # it's short-circuited in the Where class. + # We also need to handle the case where a subquery is provided entry = self.where_class() entry.add((Constraint(alias, col, None), 'isnull', True), AND) entry.negate() @@ -1732,7 +1742,7 @@ class BaseQuery(object): raise MultiJoin(pos + 1) if model: # The field lives on a base class of the current model. - # Skip the chain of proxy to the concrete proxied model + # Skip the chain of proxy to the concrete proxied model proxied_model = get_proxied_model(opts) for int_model in opts.get_base_chain(model): @@ -2362,7 +2372,7 @@ class BaseQuery(object): return cursor if result_type == SINGLE: if self.ordering_aliases: - return cursor.fetchone()[:-len(results.ordering_aliases)] + return cursor.fetchone()[:-len(self.ordering_aliases)] return cursor.fetchone() # The MULTI case. diff --git a/django/forms/models.py b/django/forms/models.py index a0b217860d..8e9c0de0e7 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -584,7 +584,7 @@ class BaseModelFormSet(BaseFormSet): else: return ugettext("Please correct the duplicate data for %(field)s, " "which must be unique.") % { - "field": get_text_list(unique_check, _("and")), + "field": get_text_list(unique_check, unicode(_("and"))), } def get_date_error_message(self, date_check): diff --git a/tests/modeltests/defer/models.py b/tests/modeltests/defer/models.py index ce65065d40..96eb427811 100644 --- a/tests/modeltests/defer/models.py +++ b/tests/modeltests/defer/models.py @@ -17,6 +17,12 @@ class Primary(models.Model): def __unicode__(self): return self.name +class Child(Primary): + pass + +class BigChild(Primary): + other = models.CharField(max_length=50) + def count_delayed_fields(obj, debug=False): """ Returns the number of delayed attributes on the given model instance. @@ -33,7 +39,7 @@ def count_delayed_fields(obj, debug=False): __test__ = {"API_TEST": """ To all outward appearances, instances with deferred fields look the same as -normal instances when we examine attribut values. Therefore we test for the +normal instances when we examine attribute values. Therefore we test for the number of deferred fields on returned instances (by poking at the internals), as a way to observe what is going on. @@ -98,5 +104,89 @@ Using defer() and only() with get() is also valid. >>> Primary.objects.all() [<Primary: a new name>] +# Regression for #10572 - A subclass with no extra fields can defer fields from the base class +>>> _ = Child.objects.create(name="c1", value="foo", related=s1) + +# You can defer a field on a baseclass when the subclass has no fields +>>> obj = Child.objects.defer("value").get(name="c1") +>>> count_delayed_fields(obj) +1 +>>> obj.name +u"c1" +>>> obj.value +u"foo" +>>> obj.name = "c2" +>>> obj.save() + +# You can retrive a single column on a base class with no fields +>>> obj = Child.objects.only("name").get(name="c2") +>>> count_delayed_fields(obj) +3 +>>> obj.name +u"c2" +>>> obj.value +u"foo" +>>> obj.name = "cc" +>>> obj.save() + +>>> _ = BigChild.objects.create(name="b1", value="foo", related=s1, other="bar") + +# You can defer a field on a baseclass +>>> obj = BigChild.objects.defer("value").get(name="b1") +>>> count_delayed_fields(obj) +1 +>>> obj.name +u"b1" +>>> obj.value +u"foo" +>>> obj.other +u"bar" +>>> obj.name = "b2" +>>> obj.save() + +# You can defer a field on a subclass +>>> obj = BigChild.objects.defer("other").get(name="b2") +>>> count_delayed_fields(obj) +1 +>>> obj.name +u"b2" +>>> obj.value +u"foo" +>>> obj.other +u"bar" +>>> obj.name = "b3" +>>> obj.save() + +# You can retrieve a single field on a baseclass +>>> obj = BigChild.objects.only("name").get(name="b3") +>>> count_delayed_fields(obj) +4 +>>> obj.name +u"b3" +>>> obj.value +u"foo" +>>> obj.other +u"bar" +>>> obj.name = "b4" +>>> obj.save() + +# You can retrieve a single field on a baseclass +>>> obj = BigChild.objects.only("other").get(name="b4") +>>> count_delayed_fields(obj) +4 +>>> obj.name +u"b4" +>>> obj.value +u"foo" +>>> obj.other +u"bar" +>>> obj.name = "bb" +>>> obj.save() + +# Finally, we need to flush the app cache for the defer module. +# Using only/defer creates some artifical entries in the app cache +# that messes up later tests. Purge all entries, just to be sure. +>>> from django.db.models.loading import cache +>>> cache.app_models['defer'] = {} """} diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 95fda273f0..0fd24c18ad 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -1175,8 +1175,9 @@ True >>> instance.height 16 -# Delete the current file since this is not done by Django. ->>> instance.image.delete() +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) >>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)}) >>> f.is_valid() @@ -1207,9 +1208,9 @@ True >>> instance.width 16 -# Delete the current image since this is not done by Django. - ->>> instance.image.delete() +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) # Override the file by uploading a new one. @@ -1224,8 +1225,9 @@ True >>> instance.width 48 -# Delete the current file since this is not done by Django. ->>> instance.image.delete() +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) >>> instance.delete() >>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data2)}) @@ -1239,8 +1241,9 @@ True >>> instance.width 48 -# Delete the current file since this is not done by Django. ->>> instance.image.delete() +# Delete the current file since this is not done by Django, but don't save +# because the dimension fields are not null=True. +>>> instance.image.delete(save=False) >>> instance.delete() # Test the non-required ImageField diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 0e28a9749f..e67126e9a7 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -536,7 +536,7 @@ class DjangoAdminSettingsDirectory(AdminScriptTestCase): args = ['startapp','settings_test'] out, err = self.run_django_admin(args,'settings') self.assertNoOutput(err) - self.assertTrue(os.path.exists(os.path.join(test_dir, 'settings_test'))) + self.assert_(os.path.exists(os.path.join(test_dir, 'settings_test'))) shutil.rmtree(os.path.join(test_dir, 'settings_test')) def test_builtin_command(self): diff --git a/tests/regressiontests/defer_regress/models.py b/tests/regressiontests/defer_regress/models.py index 11ce1557fe..da9822ab88 100644 --- a/tests/regressiontests/defer_regress/models.py +++ b/tests/regressiontests/defer_regress/models.py @@ -84,7 +84,8 @@ Some further checks for select_related() and inherited model behaviour (regression for #10710). >>> c1 = Child.objects.create(name="c1", value=42) ->>> obj = Leaf.objects.create(name="l1", child=c1) +>>> c2 = Child.objects.create(name="c2", value=37) +>>> obj = Leaf.objects.create(name="l1", child=c1, second_child=c2) >>> obj = Leaf.objects.only("name", "child").select_related()[0] >>> obj.child.name @@ -101,5 +102,24 @@ types as their non-deferred versions (bug #10738). >>> c1 is c2 is c3 True +# Regression for #10733 - only() can be used on a model with two foreign keys. +>>> results = Leaf.objects.all().only('name', 'child', 'second_child').select_related() +>>> results[0].child.name +u'c1' +>>> results[0].second_child.name +u'c2' + +>>> results = Leaf.objects.all().only('name', 'child', 'second_child', 'child__name', 'second_child__name').select_related() +>>> results[0].child.name +u'c1' +>>> results[0].second_child.name +u'c2' + +# Finally, we need to flush the app cache for the defer module. +# Using only/defer creates some artifical entries in the app cache +# that messes up later tests. Purge all entries, just to be sure. +>>> from django.db.models.loading import cache +>>> cache.app_models['defer_regress'] = {} + """ } diff --git a/tests/regressiontests/delete_regress/__init__.py b/tests/regressiontests/delete_regress/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/tests/regressiontests/delete_regress/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/regressiontests/delete_regress/models.py b/tests/regressiontests/delete_regress/models.py new file mode 100644 index 0000000000..93cadc58fa --- /dev/null +++ b/tests/regressiontests/delete_regress/models.py @@ -0,0 +1,61 @@ +from django.conf import settings +from django.db import models, backend, connection, transaction +from django.db.models import sql, query +from django.test import TransactionTestCase + +class Book(models.Model): + pagecount = models.IntegerField() + +# Can't run this test under SQLite, because you can't +# get two connections to an in-memory database. +if settings.DATABASE_ENGINE != 'sqlite3': + class DeleteLockingTest(TransactionTestCase): + def setUp(self): + # Create a second connection to the database + self.conn2 = backend.DatabaseWrapper({ + 'DATABASE_HOST': settings.DATABASE_HOST, + 'DATABASE_NAME': settings.DATABASE_NAME, + 'DATABASE_OPTIONS': settings.DATABASE_OPTIONS, + 'DATABASE_PASSWORD': settings.DATABASE_PASSWORD, + 'DATABASE_PORT': settings.DATABASE_PORT, + 'DATABASE_USER': settings.DATABASE_USER, + 'TIME_ZONE': settings.TIME_ZONE, + }) + + # Put both DB connections into managed transaction mode + transaction.enter_transaction_management() + transaction.managed(True) + self.conn2._enter_transaction_management(True) + + def tearDown(self): + # Close down the second connection. + transaction.leave_transaction_management() + self.conn2.close() + + def test_concurrent_delete(self): + "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479" + + # Create some dummy data + b1 = Book(id=1, pagecount=100) + b2 = Book(id=2, pagecount=200) + b3 = Book(id=3, pagecount=300) + b1.save() + b2.save() + b3.save() + + transaction.commit() + + self.assertEquals(3, Book.objects.count()) + + # Delete something using connection 2. + cursor2 = self.conn2.cursor() + cursor2.execute('DELETE from delete_regress_book WHERE id=1') + self.conn2._commit(); + + # Now perform a queryset delete that covers the object + # deleted in connection 2. This causes an infinite loop + # under MySQL InnoDB unless we keep track of already + # deleted objects. + Book.objects.filter(pagecount__lt=250).delete() + transaction.commit() + self.assertEquals(1, Book.objects.count()) diff --git a/tests/regressiontests/file_storage/models.py b/tests/regressiontests/file_storage/models.py index 32af5d7588..e69de29bb2 100644 --- a/tests/regressiontests/file_storage/models.py +++ b/tests/regressiontests/file_storage/models.py @@ -1,93 +0,0 @@ -import os -import tempfile -import shutil -from django.db import models -from django.core.files.storage import FileSystemStorage -from django.core.files.base import ContentFile - -# Test for correct behavior of width_field/height_field. -# Of course, we can't run this without PIL. - -try: - # Checking for the existence of Image is enough for CPython, but - # for PyPy, you need to check for the underlying modules - from PIL import Image, _imaging -except ImportError: - Image = None - -# If we have PIL, do these tests -if Image: - temp_storage_dir = tempfile.mkdtemp() - temp_storage = FileSystemStorage(temp_storage_dir) - - class Person(models.Model): - name = models.CharField(max_length=50) - mugshot = models.ImageField(storage=temp_storage, upload_to='tests', - height_field='mug_height', - width_field='mug_width') - mug_height = models.PositiveSmallIntegerField() - mug_width = models.PositiveSmallIntegerField() - - __test__ = {'API_TESTS': """ ->>> from django.core.files import File ->>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb').read() ->>> p = Person(name="Joe") ->>> p.mugshot.save("mug", ContentFile(image_data)) ->>> p.mugshot.width -16 ->>> p.mugshot.height -16 ->>> p.mug_height -16 ->>> p.mug_width -16 - -# Bug #9786: Ensure '==' and '!=' work correctly. ->>> image_data = open(os.path.join(os.path.dirname(__file__), "test1.png"), 'rb').read() ->>> p1 = Person(name="Bob") ->>> p1.mugshot.save("mug", ContentFile(image_data)) ->>> p2 = Person.objects.get(name="Joe") ->>> p.mugshot == p2.mugshot -True ->>> p.mugshot != p2.mugshot -False ->>> p.mugshot != p1.mugshot -True - -Bug #9508: Similarly to the previous test, make sure hash() works as expected -(equal items must hash to the same value). ->>> hash(p.mugshot) == hash(p2.mugshot) -True - -# Bug #8175: correctly delete files that have been removed off the file system. ->>> import os ->>> p2 = Person(name="Fred") ->>> p2.mugshot.save("shot", ContentFile(image_data)) ->>> os.remove(p2.mugshot.path) ->>> p2.delete() - -# Bug #8534: FileField.size should not leave the file open. ->>> p3 = Person(name="Joan") ->>> p3.mugshot.save("shot", ContentFile(image_data)) - -# Get a "clean" model instance ->>> p3 = Person.objects.get(name="Joan") - -# It won't have an opened file. ->>> p3.mugshot.closed -True - -# After asking for the size, the file should still be closed. ->>> _ = p3.mugshot.size ->>> p3.mugshot.closed -True - -# Make sure that wrapping the file in a file still works ->>> p3.mugshot.file.open() ->>> p = Person.objects.create(name="Bob The Builder", mugshot=File(p3.mugshot.file)) ->>> p.save() ->>> p3.mugshot.file.close() - -# Delete all test files ->>> shutil.rmtree(temp_storage_dir) -"""} diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 6e2b7be8e7..c228764e06 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -161,9 +161,9 @@ class FileStoragePathParsing(TestCase): self.storage.save('dotted.path/test', ContentFile("1")) self.storage.save('dotted.path/test', ContentFile("2")) - self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path'))) - self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test'))) - self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_'))) + self.failIf(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test_'))) def test_first_character_dot(self): """ @@ -173,13 +173,13 @@ class FileStoragePathParsing(TestCase): self.storage.save('dotted.path/.test', ContentFile("1")) self.storage.save('dotted.path/.test', ContentFile("2")) - self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test'))) # Before 2.6, a leading dot was treated as an extension, and so # underscore gets added to beginning instead of end. if sys.version_info < (2, 6): - self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_.test'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/_.test'))) else: - self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_'))) + self.assert_(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_'))) if Image is not None: class DimensionClosingBug(TestCase): diff --git a/tests/regressiontests/model_fields/4x8.png b/tests/regressiontests/model_fields/4x8.png Binary files differnew file mode 100644 index 0000000000..ffce444d48 --- /dev/null +++ b/tests/regressiontests/model_fields/4x8.png diff --git a/tests/regressiontests/model_fields/8x4.png b/tests/regressiontests/model_fields/8x4.png Binary files differnew file mode 100644 index 0000000000..60e6d69ee1 --- /dev/null +++ b/tests/regressiontests/model_fields/8x4.png diff --git a/tests/regressiontests/model_fields/imagefield.py b/tests/regressiontests/model_fields/imagefield.py new file mode 100644 index 0000000000..dd79e7aefa --- /dev/null +++ b/tests/regressiontests/model_fields/imagefield.py @@ -0,0 +1,420 @@ +import os +import shutil + +from django.core.files import File +from django.core.files.base import ContentFile +from django.core.files.images import ImageFile +from django.test import TestCase + +from models import Image, Person, PersonWithHeight, PersonWithHeightAndWidth, \ + PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile + + +# If PIL available, do these tests. +if Image: + + from models import temp_storage_dir + + + class ImageFieldTestMixin(object): + """ + Mixin class to provide common functionality to ImageField test classes. + """ + + # Person model to use for tests. + PersonModel = PersonWithHeightAndWidth + # File class to use for file instances. + File = ImageFile + + def setUp(self): + """ + Creates a pristine temp directory (or deletes and recreates if it + already exists) that the model uses as its storage directory. + + Sets up two ImageFile instances for use in tests. + """ + if os.path.exists(temp_storage_dir): + shutil.rmtree(temp_storage_dir) + os.mkdir(temp_storage_dir) + + file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png") + self.file1 = self.File(open(file_path1, 'rb')) + + file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png") + self.file2 = self.File(open(file_path2, 'rb')) + + def tearDown(self): + """ + Removes temp directory and all its contents. + """ + shutil.rmtree(temp_storage_dir) + + def check_dimensions(self, instance, width, height, + field_name='mugshot'): + """ + Asserts that the given width and height values match both the + field's height and width attributes and the height and width fields + (if defined) the image field is caching to. + + Note, this method will check for dimension fields named by adding + "_width" or "_height" to the name of the ImageField. So, the + models used in these tests must have their fields named + accordingly. + + By default, we check the field named "mugshot", but this can be + specified by passing the field_name parameter. + """ + field = getattr(instance, field_name) + # Check height/width attributes of field. + if width is None and height is None: + self.assertRaises(ValueError, getattr, field, 'width') + self.assertRaises(ValueError, getattr, field, 'height') + else: + self.assertEqual(field.width, width) + self.assertEqual(field.height, height) + + # Check height/width fields of model, if defined. + width_field_name = field_name + '_width' + if hasattr(instance, width_field_name): + self.assertEqual(getattr(instance, width_field_name), width) + height_field_name = field_name + '_height' + if hasattr(instance, height_field_name): + self.assertEqual(getattr(instance, height_field_name), height) + + + class ImageFieldTests(ImageFieldTestMixin, TestCase): + """ + Tests for ImageField that don't need to be run with each of the + different test model classes. + """ + + def test_equal_notequal_hash(self): + """ + Bug #9786: Ensure '==' and '!=' work correctly. + Bug #9508: make sure hash() works as expected (equal items must + hash to the same value). + """ + # Create two Persons with different mugshots. + p1 = self.PersonModel(name="Joe") + p1.mugshot.save("mug", self.file1) + p2 = self.PersonModel(name="Bob") + p2.mugshot.save("mug", self.file2) + self.assertEqual(p1.mugshot == p2.mugshot, False) + self.assertEqual(p1.mugshot != p2.mugshot, True) + + # Test again with an instance fetched from the db. + p1_db = self.PersonModel.objects.get(name="Joe") + self.assertEqual(p1_db.mugshot == p2.mugshot, False) + self.assertEqual(p1_db.mugshot != p2.mugshot, True) + + # Instance from db should match the local instance. + self.assertEqual(p1_db.mugshot == p1.mugshot, True) + self.assertEqual(hash(p1_db.mugshot), hash(p1.mugshot)) + self.assertEqual(p1_db.mugshot != p1.mugshot, False) + + def test_instantiate_missing(self): + """ + If the underlying file is unavailable, still create instantiate the + object without error. + """ + p = self.PersonModel(name="Joan") + p.mugshot.save("shot", self.file1) + p = self.PersonModel.objects.get(name="Joan") + path = p.mugshot.path + shutil.move(path, path + '.moved') + p2 = self.PersonModel.objects.get(name="Joan") + + def test_delete_when_missing(self): + """ + Bug #8175: correctly delete an object where the file no longer + exists on the file system. + """ + p = self.PersonModel(name="Fred") + p.mugshot.save("shot", self.file1) + os.remove(p.mugshot.path) + p.delete() + + def test_size_method(self): + """ + Bug #8534: FileField.size should not leave the file open. + """ + p = self.PersonModel(name="Joan") + p.mugshot.save("shot", self.file1) + + # Get a "clean" model instance + p = self.PersonModel.objects.get(name="Joan") + # It won't have an opened file. + self.assertEqual(p.mugshot.closed, True) + + # After asking for the size, the file should still be closed. + _ = p.mugshot.size + self.assertEqual(p.mugshot.closed, True) + + def test_pickle(self): + """ + Tests that ImageField can be pickled, unpickled, and that the + image of the unpickled version is the same as the original. + """ + import pickle + + p = Person(name="Joe") + p.mugshot.save("mug", self.file1) + dump = pickle.dumps(p) + + p2 = Person(name="Bob") + p2.mugshot = self.file1 + + loaded_p = pickle.loads(dump) + self.assertEqual(p.mugshot, loaded_p.mugshot) + + + class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase): + """ + Tests behavior of an ImageField and its dimensions fields. + """ + + def test_constructor(self): + """ + Tests assigning an image field through the model's constructor. + """ + p = self.PersonModel(name='Joe', mugshot=self.file1) + self.check_dimensions(p, 4, 8) + p.save() + self.check_dimensions(p, 4, 8) + + def test_image_after_constructor(self): + """ + Tests behavior when image is not passed in constructor. + """ + p = self.PersonModel(name='Joe') + # TestImageField value will default to being an instance of its + # attr_class, a TestImageFieldFile, with name == None, which will + # cause it to evaluate as False. + self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True) + self.assertEqual(bool(p.mugshot), False) + + # Test setting a fresh created model instance. + p = self.PersonModel(name='Joe') + p.mugshot = self.file1 + self.check_dimensions(p, 4, 8) + + def test_create(self): + """ + Tests assigning an image in Manager.create(). + """ + p = self.PersonModel.objects.create(name='Joe', mugshot=self.file1) + self.check_dimensions(p, 4, 8) + + def test_default_value(self): + """ + Tests that the default value for an ImageField is an instance of + the field's attr_class (TestImageFieldFile in this case) with no + name (name set to None). + """ + p = self.PersonModel() + self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True) + self.assertEqual(bool(p.mugshot), False) + + def test_assignment_to_None(self): + """ + Tests that assigning ImageField to None clears dimensions. + """ + p = self.PersonModel(name='Joe', mugshot=self.file1) + self.check_dimensions(p, 4, 8) + + # If image assigned to None, dimension fields should be cleared. + p.mugshot = None + self.check_dimensions(p, None, None) + + p.mugshot = self.file2 + self.check_dimensions(p, 8, 4) + + def test_field_save_and_delete_methods(self): + """ + Tests assignment using the field's save method and deletion using + the field's delete method. + """ + p = self.PersonModel(name='Joe') + p.mugshot.save("mug", self.file1) + self.check_dimensions(p, 4, 8) + + # A new file should update dimensions. + p.mugshot.save("mug", self.file2) + self.check_dimensions(p, 8, 4) + + # Field and dimensions should be cleared after a delete. + p.mugshot.delete(save=False) + self.assertEqual(p.mugshot, None) + self.check_dimensions(p, None, None) + + def test_dimensions(self): + """ + Checks that dimensions are updated correctly in various situations. + """ + p = self.PersonModel(name='Joe') + + # Dimensions should get set if file is saved. + p.mugshot.save("mug", self.file1) + self.check_dimensions(p, 4, 8) + + # Test dimensions after fetching from database. + p = self.PersonModel.objects.get(name='Joe') + # Bug 11084: Dimensions should not get recalculated if file is + # coming from the database. We test this by checking if the file + # was opened. + self.assertEqual(p.mugshot.was_opened, False) + self.check_dimensions(p, 4, 8) + # After checking dimensions on the image field, the file will have + # opened. + self.assertEqual(p.mugshot.was_opened, True) + # Dimensions should now be cached, and if we reset was_opened and + # check dimensions again, the file should not have opened. + p.mugshot.was_opened = False + self.check_dimensions(p, 4, 8) + self.assertEqual(p.mugshot.was_opened, False) + + # If we assign a new image to the instance, the dimensions should + # update. + p.mugshot = self.file2 + self.check_dimensions(p, 8, 4) + # Dimensions were recalculated, and hence file should have opened. + self.assertEqual(p.mugshot.was_opened, True) + + + class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField with no dimension fields. + """ + + PersonModel = Person + + + class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField with one dimensions field. + """ + + PersonModel = PersonWithHeight + + + class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField where the dimensions fields are + defined before the ImageField. + """ + + PersonModel = PersonDimensionsFirst + + + class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests): + """ + Tests behavior of an ImageField when assigning it a File instance + rather than an ImageFile instance. + """ + + PersonModel = PersonDimensionsFirst + File = File + + + class TwoImageFieldTests(ImageFieldTestMixin, TestCase): + """ + Tests a model with two ImageFields. + """ + + PersonModel = PersonTwoImages + + def test_constructor(self): + p = self.PersonModel(mugshot=self.file1, headshot=self.file2) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + p.save() + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + def test_create(self): + p = self.PersonModel.objects.create(mugshot=self.file1, + headshot=self.file2) + self.check_dimensions(p, 4, 8) + self.check_dimensions(p, 8, 4, 'headshot') + + def test_assignment(self): + p = self.PersonModel() + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + + p.mugshot = self.file1 + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + p.headshot = self.file2 + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + # Clear the ImageFields one at a time. + p.mugshot = None + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + p.headshot = None + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + + def test_field_save_and_delete_methods(self): + p = self.PersonModel(name='Joe') + p.mugshot.save("mug", self.file1) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + p.headshot.save("head", self.file2) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + # We can use save=True when deleting the image field with null=True + # dimension fields and the other field has an image. + p.headshot.delete(save=True) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + p.mugshot.delete(save=False) + self.check_dimensions(p, None, None, 'mugshot') + self.check_dimensions(p, None, None, 'headshot') + + def test_dimensions(self): + """ + Checks that dimensions are updated correctly in various situations. + """ + p = self.PersonModel(name='Joe') + + # Dimensions should get set for the saved file. + p.mugshot.save("mug", self.file1) + p.headshot.save("head", self.file2) + self.check_dimensions(p, 4, 8, 'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + + # Test dimensions after fetching from database. + p = self.PersonModel.objects.get(name='Joe') + # Bug 11084: Dimensions should not get recalculated if file is + # coming from the database. We test this by checking if the file + # was opened. + self.assertEqual(p.mugshot.was_opened, False) + self.assertEqual(p.headshot.was_opened, False) + self.check_dimensions(p, 4, 8,'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + # After checking dimensions on the image fields, the files will + # have been opened. + self.assertEqual(p.mugshot.was_opened, True) + self.assertEqual(p.headshot.was_opened, True) + # Dimensions should now be cached, and if we reset was_opened and + # check dimensions again, the file should not have opened. + p.mugshot.was_opened = False + p.headshot.was_opened = False + self.check_dimensions(p, 4, 8,'mugshot') + self.check_dimensions(p, 8, 4, 'headshot') + self.assertEqual(p.mugshot.was_opened, False) + self.assertEqual(p.headshot.was_opened, False) + + # If we assign a new image to the instance, the dimensions should + # update. + p.mugshot = self.file2 + p.headshot = self.file1 + self.check_dimensions(p, 8, 4, 'mugshot') + self.check_dimensions(p, 4, 8, 'headshot') + # Dimensions were recalculated, and hence file should have opened. + self.assertEqual(p.mugshot.was_opened, True) + self.assertEqual(p.headshot.was_opened, True) diff --git a/tests/regressiontests/model_fields/models.py b/tests/regressiontests/model_fields/models.py index 43ef667d00..81e6c22f09 100644 --- a/tests/regressiontests/model_fields/models.py +++ b/tests/regressiontests/model_fields/models.py @@ -1,10 +1,23 @@ -from django.db import models +import os +import tempfile try: import decimal except ImportError: from django.utils import _decimal as decimal # Python 2.3 fallback +try: + # Checking for the existence of Image is enough for CPython, but for PyPy, + # you need to check for the underlying modules. + from PIL import Image, _imaging +except ImportError: + Image = None + +from django.core.files.storage import FileSystemStorage +from django.db import models +from django.db.models.fields.files import ImageFieldFile, ImageField + + class Foo(models.Model): a = models.CharField(max_length=10) d = models.DecimalField(max_digits=5, decimal_places=3) @@ -31,9 +44,97 @@ class Whiz(models.Model): (0,'Other'), ) c = models.IntegerField(choices=CHOICES, null=True) - + class BigD(models.Model): d = models.DecimalField(max_digits=38, decimal_places=30) class BigS(models.Model): - s = models.SlugField(max_length=255)
\ No newline at end of file + s = models.SlugField(max_length=255) + + +############################################################################### +# ImageField + +# If PIL available, do these tests. +if Image: + class TestImageFieldFile(ImageFieldFile): + """ + Custom Field File class that records whether or not the underlying file + was opened. + """ + def __init__(self, *args, **kwargs): + self.was_opened = False + super(TestImageFieldFile, self).__init__(*args,**kwargs) + def open(self): + self.was_opened = True + super(TestImageFieldFile, self).open() + + class TestImageField(ImageField): + attr_class = TestImageFieldFile + + # Set up a temp directory for file storage. + temp_storage_dir = tempfile.mkdtemp() + temp_storage = FileSystemStorage(temp_storage_dir) + temp_upload_to_dir = os.path.join(temp_storage.location, 'tests') + + class Person(models.Model): + """ + Model that defines an ImageField with no dimension fields. + """ + name = models.CharField(max_length=50) + mugshot = TestImageField(storage=temp_storage, upload_to='tests') + + class PersonWithHeight(models.Model): + """ + Model that defines an ImageField with only one dimension field. + """ + name = models.CharField(max_length=50) + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height') + mugshot_height = models.PositiveSmallIntegerField() + + class PersonWithHeightAndWidth(models.Model): + """ + Model that defines height and width fields after the ImageField. + """ + name = models.CharField(max_length=50) + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height', + width_field='mugshot_width') + mugshot_height = models.PositiveSmallIntegerField() + mugshot_width = models.PositiveSmallIntegerField() + + class PersonDimensionsFirst(models.Model): + """ + Model that defines height and width fields before the ImageField. + """ + name = models.CharField(max_length=50) + mugshot_height = models.PositiveSmallIntegerField() + mugshot_width = models.PositiveSmallIntegerField() + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height', + width_field='mugshot_width') + + class PersonTwoImages(models.Model): + """ + Model that: + * Defines two ImageFields + * Defines the height/width fields before the ImageFields + * Has a nullalble ImageField + """ + name = models.CharField(max_length=50) + mugshot_height = models.PositiveSmallIntegerField() + mugshot_width = models.PositiveSmallIntegerField() + mugshot = TestImageField(storage=temp_storage, upload_to='tests', + height_field='mugshot_height', + width_field='mugshot_width') + headshot_height = models.PositiveSmallIntegerField( + blank=True, null=True) + headshot_width = models.PositiveSmallIntegerField( + blank=True, null=True) + headshot = TestImageField(blank=True, null=True, + storage=temp_storage, upload_to='tests', + height_field='headshot_height', + width_field='headshot_width') + +############################################################################### diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py index 0815d9ef3c..7a6fee5a2a 100644 --- a/tests/regressiontests/model_fields/tests.py +++ b/tests/regressiontests/model_fields/tests.py @@ -6,13 +6,26 @@ from django import forms from django.db import models from django.core.exceptions import ValidationError -from models import Foo, Bar, Whiz, BigD, BigS +from models import Foo, Bar, Whiz, BigD, BigS, Image try: from decimal import Decimal except ImportError: from django.utils._decimal import Decimal + +# If PIL available, do these tests. +if Image: + from imagefield import \ + ImageFieldTests, \ + ImageFieldTwoDimensionsTests, \ + ImageFieldNoDimensionsTests, \ + ImageFieldOneDimensionTests, \ + ImageFieldDimensionsFirstTests, \ + ImageFieldUsingFileTests, \ + TwoImageFieldTests + + class DecimalFieldTests(django.test.TestCase): def test_to_python(self): f = models.DecimalField(max_digits=4, decimal_places=2) @@ -131,4 +144,3 @@ class SlugFieldTests(django.test.TestCase): bs = BigS.objects.create(s = 'slug'*50) bs = BigS.objects.get(pk=bs.pk) self.assertEqual(bs.s, 'slug'*50) - diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index b5fa377496..3649d27171 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -1143,6 +1143,33 @@ True >>> r.save() >>> Ranking.objects.all() [<Ranking: 3: a1>, <Ranking: 2: a2>, <Ranking: 1: a3>] + +# Regression test for #10742: +# Queries used in an __in clause don't execute subqueries + +>>> subq = Author.objects.filter(num__lt=3000) +>>> qs = Author.objects.filter(pk__in=subq) +>>> list(qs) +[<Author: a1>, <Author: a2>] +# The subquery result cache should not be populated +>>> subq._result_cache is None +True + +>>> subq = Author.objects.filter(num__lt=3000) +>>> qs = Author.objects.exclude(pk__in=subq) +>>> list(qs) +[<Author: a3>, <Author: a4>] +# The subquery result cache should not be populated +>>> subq._result_cache is None +True + +>>> subq = Author.objects.filter(num__lt=3000) +>>> list(Author.objects.filter(Q(pk__in=subq) & Q(name='a1'))) +[<Author: a1>] +# The subquery result cache should not be populated +>>> subq._result_cache is None +True + """} # In Python 2.3 and the Python 2.6 beta releases, exceptions raised in __len__ |
