diff options
| author | Adrian Holovaty <adrian@holovaty.com> | 2006-05-02 01:31:56 +0000 |
|---|---|---|
| committer | Adrian Holovaty <adrian@holovaty.com> | 2006-05-02 01:31:56 +0000 |
| commit | f69cf70ed813a8cd7e1f963a14ae39103e8d5265 (patch) | |
| tree | d3b32e84cd66573b3833ddf662af020f8ef2f7a8 /django | |
| parent | d5dbeaa9be359a4c794885c2e9f1b5a7e5e51fb8 (diff) | |
MERGED MAGIC-REMOVAL BRANCH TO TRUNK. This change is highly backwards-incompatible. Please read http://code.djangoproject.com/wiki/RemovingTheMagic for upgrade instructions.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@2809 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
227 files changed, 9581 insertions, 6974 deletions
diff --git a/django/__init__.py b/django/__init__.py index 593e2f46e4..00c6f82478 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 1, 'SVN') +VERSION = (0, 95, 'post-magic-removal') diff --git a/django/bin/daily_cleanup.py b/django/bin/daily_cleanup.py index 52e2ef43fd..6eb5c17feb 100644 --- a/django/bin/daily_cleanup.py +++ b/django/bin/daily_cleanup.py @@ -1,17 +1,17 @@ "Daily cleanup file" -from django.core.db import db +from django.db import backend, connection, transaction DOCUMENTATION_DIRECTORY = '/home/html/documentation/' def clean_up(): # Clean up old database records - cursor = db.cursor() + cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE %s < NOW()" % \ - (db.quote_name('core_sessions'), db.quote_name('expire_date'))) + (backend.quote_name('core_sessions'), backend.quote_name('expire_date'))) cursor.execute("DELETE FROM %s WHERE %s < NOW() - INTERVAL '1 week'" % \ - (db.quote_name('registration_challenges'), db.quote_name('request_date'))) - db.commit() + (backend.quote_name('registration_challenges'), backend.quote_name('request_date'))) + transaction.commit_unless_managed() if __name__ == "__main__": clean_up() diff --git a/django/conf/__init__.py b/django/conf/__init__.py index e69de29bb2..291ba8ce3f 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -0,0 +1,73 @@ +""" +Settings and configuration for Django. + +Values will be read from the module specified by the DJANGO_SETTINGS_MODULE environment +variable, and then from django.conf.global_settings; see the global settings file for +a list of all possible variables. +""" + +import os +import sys +from django.conf import global_settings + +ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE" + +class Settings: + + def __init__(self, settings_module): + + # update this dict from global settings (but only for ALL_CAPS settings) + for setting in dir(global_settings): + if setting == setting.upper(): + setattr(self, setting, getattr(global_settings, setting)) + + # store the settings module in case someone later cares + self.SETTINGS_MODULE = settings_module + + try: + mod = __import__(self.SETTINGS_MODULE, '', '', ['']) + except ImportError, e: + raise EnvironmentError, "Could not import settings '%s' (is it on sys.path?): %s" % (self.SETTINGS_MODULE, e) + + # Settings that should be converted into tuples if they're mistakenly entered + # as strings. + tuple_settings = ("INSTALLED_APPS", "TEMPLATE_DIRS") + + for setting in dir(mod): + if setting == setting.upper(): + setting_value = getattr(mod, setting) + if setting in tuple_settings and type(setting_value) == str: + setting_value = (setting_value,) # In case the user forgot the comma. + setattr(self, setting, setting_value) + + # Expand entries in INSTALLED_APPS like "django.contrib.*" to a list + # of all those apps. + new_installed_apps = [] + for app in self.INSTALLED_APPS: + if app.endswith('.*'): + appdir = os.path.dirname(__import__(app[:-2], '', '', ['']).__file__) + for d in os.listdir(appdir): + if d.isalpha() and os.path.isdir(os.path.join(appdir, d)): + new_installed_apps.append('%s.%s' % (app[:-2], d)) + else: + new_installed_apps.append(app) + self.INSTALLED_APPS = new_installed_apps + + # move the time zone info into os.environ + os.environ['TZ'] = self.TIME_ZONE + +# try to load DJANGO_SETTINGS_MODULE +try: + settings_module = os.environ[ENVIRONMENT_VARIABLE] + if not settings_module: # If it's set but is an empty string. + raise KeyError +except KeyError: + raise EnvironmentError, "Environment variable %s is undefined." % ENVIRONMENT_VARIABLE + +# instantiate the configuration object +settings = Settings(settings_module) + +# install the translation machinery so that it is available +from django.utils import translation +translation.install() + diff --git a/django/conf/app_template/models.py b/django/conf/app_template/models.py new file mode 100644 index 0000000000..71a8362390 --- /dev/null +++ b/django/conf/app_template/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django/conf/app_template/models/__init__.py b/django/conf/app_template/models/__init__.py deleted file mode 100644 index 502a7d0738..0000000000 --- a/django/conf/app_template/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['{{ app_name }}'] diff --git a/django/conf/app_template/models/app_name.py b/django/conf/app_template/models/app_name.py deleted file mode 100644 index 6fce302e01..0000000000 --- a/django/conf/app_template/models/app_name.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.core import meta - -# Create your models here. diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index f6ec3d2611..cfa174287b 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -79,7 +79,7 @@ SERVER_EMAIL = 'root@localhost' SEND_BROKEN_LINK_EMAILS = False # Database connection info. -DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. +DATABASE_ENGINE = '' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. @@ -102,19 +102,16 @@ INSTALLED_APPS = () # List of locations of the template source files, in search order. TEMPLATE_DIRS = () -# Extension on all templates. -TEMPLATE_FILE_EXTENSION = '.html' - # List of callables that know how to import templates from various sources. # See the comments in django/core/template/loader.py for interface # documentation. TEMPLATE_LOADERS = ( - 'django.core.template.loaders.filesystem.load_template_source', - 'django.core.template.loaders.app_directories.load_template_source', -# 'django.core.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', ) -# List of processors used by DjangoContext to populate the context. +# List of processors used by RequestContext to populate the context. # Each one should be a callable that takes the request object as its # only parameter and returns a dictionary to add to the context. TEMPLATE_CONTEXT_PROCESSORS = ( @@ -205,6 +202,10 @@ TIME_FORMAT = 'P' # http://psyco.sourceforge.net/ ENABLE_PSYCO = False +# Do you want to manage transactions manually? +# Hint: you really don't! +TRANSACTIONS_MANAGED = False + ############## # MIDDLEWARE # ############## @@ -213,7 +214,8 @@ ENABLE_PSYCO = False # this middleware classes will be applied in the order given, and in the # response phase the middleware will be applied in reverse order. MIDDLEWARE_CLASSES = ( - "django.middleware.sessions.SessionMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", # "django.middleware.http.ConditionalGetMiddleware", # "django.middleware.gzip.GZipMiddleware", "django.middleware.common.CommonMiddleware", diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index 71d4da472d..800d1023d9 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -9,7 +9,7 @@ ADMINS = ( MANAGERS = ADMINS -DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. +DATABASE_ENGINE = '' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. @@ -45,14 +45,15 @@ SECRET_KEY = '' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.core.template.loaders.filesystem.load_template_source', - 'django.core.template.loaders.app_directories.load_template_source', -# 'django.core.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( "django.middleware.common.CommonMiddleware", - "django.middleware.sessions.SessionMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", "django.middleware.doc.XViewMiddleware", ) @@ -64,4 +65,8 @@ TEMPLATE_DIRS = ( ) INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', ) diff --git a/django/conf/project_template/urls.py b/django/conf/project_template/urls.py index fe77ea1867..4483014173 100644 --- a/django/conf/project_template/urls.py +++ b/django/conf/project_template/urls.py @@ -5,5 +5,5 @@ urlpatterns = patterns('', # (r'^{{ project_name }}/', include('{{ project_name }}.apps.foo.urls.foo')), # Uncomment this for admin: -# (r'^admin/', include('django.contrib.admin.urls.admin')), +# (r'^admin/', include('django.contrib.admin.urls')), ) diff --git a/django/conf/settings.py b/django/conf/settings.py deleted file mode 100644 index f455f65afe..0000000000 --- a/django/conf/settings.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Settings and configuration for Django. - -Values will be read from the module specified by the DJANGO_SETTINGS_MODULE environment -variable, and then from django.conf.global_settings; see the global settings file for -a list of all possible variables. -""" - -import os -import sys -from django.conf import global_settings - -ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE" - -# get a reference to this module (why isn't there a __module__ magic var?) -me = sys.modules[__name__] - -# update this dict from global settings (but only for ALL_CAPS settings) -for setting in dir(global_settings): - if setting == setting.upper(): - setattr(me, setting, getattr(global_settings, setting)) - -# try to load DJANGO_SETTINGS_MODULE -try: - me.SETTINGS_MODULE = os.environ[ENVIRONMENT_VARIABLE] - if not me.SETTINGS_MODULE: # If it's set but is an empty string. - raise KeyError -except KeyError: - raise EnvironmentError, "Environment variable %s is undefined." % ENVIRONMENT_VARIABLE - -try: - mod = __import__(me.SETTINGS_MODULE, '', '', ['']) -except ImportError, e: - raise EnvironmentError, "Could not import %s '%s' (is it on sys.path?): %s" % (ENVIRONMENT_VARIABLE, me.SETTINGS_MODULE, e) - -# Settings that should be converted into tuples if they're mistakenly entered -# as strings. -tuple_settings = ("INSTALLED_APPS", "TEMPLATE_DIRS") - -for setting in dir(mod): - if setting == setting.upper(): - setting_value = getattr(mod, setting) - if setting in tuple_settings and type(setting_value) == str: - setting_value = (setting_value,) # In case the user forgot the comma. - setattr(me, setting, setting_value) - -# Expand entries in INSTALLED_APPS like "django.contrib.*" to a list -# of all those apps. -new_installed_apps = [] -for app in me.INSTALLED_APPS: - if app.endswith('.*'): - appdir = os.path.dirname(__import__(app[:-2], '', '', ['']).__file__) - for d in os.listdir(appdir): - if d.isalpha() and os.path.isdir(os.path.join(appdir, d)): - new_installed_apps.append('%s.%s' % (app[:-2], d)) - else: - new_installed_apps.append(app) -me.INSTALLED_APPS = new_installed_apps - -# save DJANGO_SETTINGS_MODULE in case anyone in the future cares -me.SETTINGS_MODULE = os.environ.get(ENVIRONMENT_VARIABLE, '') - -# move the time zone info into os.environ -os.environ['TZ'] = me.TIME_ZONE - -# finally, clean up my namespace -for k in dir(me): - if not k.startswith('_') and k != 'me' and k != k.upper(): - delattr(me, k) -del me, k - -# as the last step, install the translation machinery and -# remove the module again to not clutter the namespace. -from django.utils import translation -translation.install() -del translation - diff --git a/django/conf/urls/registration.py b/django/conf/urls/registration.py index 5a56fe5e05..2d733a898b 100644 --- a/django/conf/urls/registration.py +++ b/django/conf/urls/registration.py @@ -1,9 +1,9 @@ from django.conf.urls.defaults import * urlpatterns = patterns('', - (r'^login/$', 'django.views.auth.login.login'), - (r'^logout/$', 'django.views.auth.login.logout'), - (r'^login_another/$', 'django.views.auth.login.logout_then_login'), + (r'^login/$', 'django.contrib.auth.view.login'), + (r'^logout/$', 'django.contrib.auth.views.logout'), + (r'^login_another/$', 'django.contrib.auth.views.logout_then_login'), (r'^register/$', 'ellington.registration.views.registration.signup'), (r'^register/(?P<challenge_string>\w{32})/$', 'ellington.registration.views.registration.register_form'), @@ -12,8 +12,8 @@ urlpatterns = patterns('', (r'^profile/welcome/$', 'ellington.registration.views.profile.profile_welcome'), (r'^profile/edit/$', 'ellington.registration.views.profile.edit_profile'), - (r'^password_reset/$', 'django.views.registration.passwords.password_reset'), - (r'^password_reset/done/$', 'django.views.registration.passwords.password_reset_done'), - (r'^password_change/$', 'django.views.registration.passwords.password_change'), - (r'^password_change/done/$', 'django.views.registration.passwords.password_change_done'), + (r'^password_reset/$', 'django.contrib.auth.views.password_reset'), + (r'^password_reset/done/$', 'django.contrib.auth.views.password_reset_done'), + (r'^password_change/$', 'django.contrib.auth.views.password_change'), + (r'^password_change/done/$', 'django.contrib.auth.views.password_change_done'), ) diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py index 31a6ba37c9..0284f13114 100644 --- a/django/contrib/admin/filterspecs.py +++ b/django/contrib/admin/filterspecs.py @@ -6,7 +6,7 @@ Each filter subclass knows how to display a filter for a field that passes a certain test -- e.g. being a DateField or ForeignKey. """ -from django.core import meta +from django.db import models import datetime class FilterSpec(object): @@ -50,13 +50,13 @@ class FilterSpec(object): class RelatedFilterSpec(FilterSpec): def __init__(self, f, request, params): super(RelatedFilterSpec, self).__init__(f, request, params) - if isinstance(f, meta.ManyToManyField): - self.lookup_title = f.rel.to.verbose_name + if isinstance(f, models.ManyToManyField): + self.lookup_title = f.rel.to._meta.verbose_name else: self.lookup_title = f.verbose_name - self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to.pk.name) + self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to._meta.pk.name) self.lookup_val = request.GET.get(self.lookup_kwarg, None) - self.lookup_choices = f.rel.to.get_model_module().get_list() + self.lookup_choices = f.rel.to._default_manager.all() def has_output(self): return len(self.lookup_choices) > 1 @@ -69,7 +69,7 @@ class RelatedFilterSpec(FilterSpec): 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), 'display': _('All')} for val in self.lookup_choices: - pk_val = getattr(val, self.field.rel.to.pk.attname) + pk_val = getattr(val, self.field.rel.to._meta.pk.attname) yield {'selected': self.lookup_val == str(pk_val), 'query_string': cl.get_query_string( {self.lookup_kwarg: pk_val}), 'display': val} @@ -103,7 +103,7 @@ class DateFieldFilterSpec(FilterSpec): today = datetime.date.today() one_week_ago = today - datetime.timedelta(days=7) - today_str = isinstance(self.field, meta.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d') + today_str = isinstance(self.field, models.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d') self.links = ( (_('Any date'), {}), @@ -126,7 +126,7 @@ class DateFieldFilterSpec(FilterSpec): 'query_string': cl.get_query_string( param_dict, self.field_generic), 'display': title} -FilterSpec.register(lambda f: isinstance(f, meta.DateField), DateFieldFilterSpec) +FilterSpec.register(lambda f: isinstance(f, models.DateField), DateFieldFilterSpec) class BooleanFieldFilterSpec(FilterSpec): def __init__(self, f, request, params): @@ -144,9 +144,9 @@ class BooleanFieldFilterSpec(FilterSpec): yield {'selected': self.lookup_val == v and not self.lookup_val2, 'query_string': cl.get_query_string( {self.lookup_kwarg: v}, [self.lookup_kwarg2]), 'display': k} - if isinstance(self.field, meta.NullBooleanField): + if isinstance(self.field, models.NullBooleanField): yield {'selected': self.lookup_val2 == 'True', 'query_string': cl.get_query_string( {self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]), 'display': _('Unknown')} -FilterSpec.register(lambda f: isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField), BooleanFieldFilterSpec) +FilterSpec.register(lambda f: isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField), BooleanFieldFilterSpec) diff --git a/django/contrib/admin/media/css/base.css b/django/contrib/admin/media/css/base.css index 7c4437f454..88f7d9a95a 100644 --- a/django/contrib/admin/media/css/base.css +++ b/django/contrib/admin/media/css/base.css @@ -1,3 +1,14 @@ -@import url(global.css); -@import url(changelists.css); +/* + DJANGO Admin + by Wilson Miner wilson@lawrence.com +*/ + +/* Block IE 5 */ +@import "null?\"\{"; + +/* Import other styles */ +@import url('global.css'); +@import url('layout.css'); + +/* Import patch for IE 6 Windows */ /*\*/ @import "patch-iewin.css"; /**/
\ No newline at end of file diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 7ff59c5e6b..2269c9fe20 100644 --- a/django/contrib/admin/media/css/changelists.css +++ b/django/contrib/admin/media/css/changelists.css @@ -1,16 +1,13 @@ -/* - DJANGO Admin Changelist Styles - by Wilson Miner wilson@lawrence.com - Copyright (c) 2005 Lawrence Journal-World -*/ +@import url('base.css'); +/* CHANGELISTS */ #changelist { position:relative; width:100%; } #changelist table { width:100%; } .change-list .filtered table { border-right:1px solid #ddd; } .change-list .filtered { min-height:400px; _height:400px; } .change-list .filtered { background:white url(../img/admin/changelist-bg.gif) top right repeat-y !important; } .change-list .filtered table, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { margin-right:160px !important; width:auto !important; } -.change-list .filtered table tbody th { padding-right:10px; } +.change-list .filtered table tbody th { padding-right:1em; } #changelist .toplinks { border-bottom:1px solid #ccc !important; } #changelist .paginator { color:#666; border-top:1px solid #eee; border-bottom:1px solid #eee; background:white url(../img/admin/nav-bg.gif) 0 180% repeat-x; overflow:hidden; } .change-list .filtered .paginator { border-right:1px solid #ddd; } @@ -42,3 +39,12 @@ .change-list ul.toplinks li { float: left; width: 9em; padding:3px 6px; font-weight: bold; list-style-type:none; } .change-list ul.toplinks .date-back a { color:#999; } .change-list ul.toplinks .date-back a:hover { color:#036; } + +/* PAGINATOR */ +.paginator { font-size:11px; padding-top:10px; padding-bottom:10px; line-height:22px; margin:0; border-top:1px solid #ddd; } +.paginator a:link, .paginator a:visited { padding:2px 6px; border:solid 1px #ccc; background:white; text-decoration:none; } +.paginator a.showall { padding:0 !important; border:none !important; } +.paginator a.showall:hover { color:#036 !important; background:transparent !important; } +.paginator .end { border-width:2px !important; margin-right:6px; } +.paginator .this-page { padding:2px 6px; font-weight:bold; font-size:13px; vertical-align:top; } +.paginator a:hover { color:white; background:#5b80b2; border-color:#036; } diff --git a/django/contrib/admin/media/css/dashboard.css b/django/contrib/admin/media/css/dashboard.css new file mode 100644 index 0000000000..d27797324b --- /dev/null +++ b/django/contrib/admin/media/css/dashboard.css @@ -0,0 +1,10 @@ +@import url('base.css'); + +/* DASHBOARD */ +.dashboard .module table th { width:100%; } +.dashboard .module table td { white-space:nowrap; } +.dashboard .module table td a { display:block; padding-right:.6em; } + +/* RECENT ACTIONS MODULE */ +.module ul.actionlist { margin-left:0; } +ul.actionlist li { list-style-type:none; }
\ No newline at end of file diff --git a/django/contrib/admin/media/css/forms.css b/django/contrib/admin/media/css/forms.css new file mode 100644 index 0000000000..b66f268fec --- /dev/null +++ b/django/contrib/admin/media/css/forms.css @@ -0,0 +1,60 @@ +@import url('base.css'); +@import url('widgets.css'); + +/* FORM ROWS */ +.form-row { overflow:hidden; padding:8px 12px; font-size:11px; border-bottom:1px solid #eee; } +.form-row img, .form-row input { vertical-align:middle; } +form .form-row p { padding-left:0; font-size:11px; } + +/* FORM LABELS */ +form h4 { margin:0 !important; padding:0 !important; border:none !important; } +label { font-weight:normal !important; color:#666; font-size:12px; } +label.inline { margin-left:20px; } +.required label, label.required { font-weight:bold !important; color:#333 !important; } + +/* RADIO BUTTONS */ +form ul.radiolist li { list-style-type:none; } +form ul.radiolist label { float:none; display:inline; } +form ul.inline { margin-left:0; padding:0; } +form ul.inline li { float:left; padding-right:7px; } + +/* ALIGNED FIELDSETS */ +.aligned label { display:block; padding:0 1em 3px 0; float:left; width:8em; } +.aligned label.inline { display:inline; float:none; } +.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { width:350px; } +form .aligned p, form .aligned ul { margin-left:7em; padding-left:30px; } +form .aligned table p { margin-left:0; padding-left:0; } +form .aligned p.help { padding-left:38px; } +.aligned .vCheckboxLabel { float:none !important; display:inline; padding-left:4px; } +.colM .aligned .vLargeTextField, colM .aligned .vXMLLargeTextField { width:610px; } +.checkbox-row p.help { margin-left:0; padding-left:0 !important; } + +/* WIDE FIELDSETS */ +.wide label { width:15em !important; } +form .wide p { margin-left:15em; } +form .wide p.help { padding-left:38px; } +.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { width:450px; } + +/* COLLAPSED FIELDSETS */ +fieldset.collapsed * { display:none; } +fieldset.collapsed h2, fieldset.collapsed { display:block !important; } +fieldset.collapsed h2 { background-image:url(../img/admin/nav-bg.gif); background-position:bottom left; color:#999; } +fieldset.collapsed .collapse-toggle { padding:3px 5px !important; background:transparent; display:inline !important;} + +/* MONOSPACE TEXTAREAS */ +fieldset.monospace textarea { font-family:"Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace; } + +/* SUBMIT ROW */ +.submit-row { padding:5px 7px; text-align:right; background:white url(../img/admin/nav-bg.gif) 0 100% repeat-x; border:1px solid #ccc; margin:5px 0; } +.submit-row input { margin:0 0 0 5px; } +.submit-row p { margin-top:0.3em; } +.submit-row .deletelink { background:url(../img/admin/icon_deletelink.gif) 0 50% no-repeat; padding-left:14px; } + +/* CUSTOM FORM FIELDS */ +.vSelectMultipleField { vertical-align:top !important; } +.vCheckboxField { border:none; } +.vDateField, .vTimeField { margin-right:2px; } +.vURLField { width:30em; } +.vLargeTextField, .vXMLLargeTextField { width:48em; } +.flatpages-flatpage #id_content { height:40.2em; } +.module table .vPositiveSmallIntegerField { width:2.2em; }
\ No newline at end of file diff --git a/django/contrib/admin/media/css/global.css b/django/contrib/admin/media/css/global.css index 765e752d48..67e37324e5 100644 --- a/django/contrib/admin/media/css/global.css +++ b/django/contrib/admin/media/css/global.css @@ -1,19 +1,14 @@ -/* - DJANGO Admin Global Styles - by Wilson Miner wilson@lawrence.com - Copyright (c) 2005 Lawrence Journal-World -*/ - -body { margin:0; padding:0; font-family:"Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:#fff; } +body { margin:0; padding:0; font-size:12px; font-family:"Lucida Grande","Bitstream Vera Sans",Verdana,Arial,sans-serif; color:#333; background:#fff; } /* LINKS */ a:link, a:visited { color: #5b80b2; text-decoration:none; } a:hover { color: #036; } a img { border:none; } -/* GLOBAL DEFAULTS */ -p, ol, ul, dl { margin:.2em 0 .8em 0; font-size:12px; } +/* GLOBAL DEFAULTS */ +p, ol, ul, dl { margin:.2em 0 .8em 0; } p { padding:0; line-height:140%; } + h1,h2,h3,h4,h5 { font-weight:bold; } h1 { font-size:18px; color:#666; padding:0 6px 0 0; margin:0 0 .2em 0; } h2 { font-size:16px; margin:1em 0 .5em 0; } @@ -21,6 +16,7 @@ h2.subhead { font-weight:normal;margin-top:0; } h3 { font-size:14px; margin:.8em 0 .3em 0; color:#666; font-weight:bold; } h4 { font-size:12px; margin:1em 0 .8em 0; padding-bottom:3px; } h5 { font-size:10px; margin:1.5em 0 .5em 0; color:#666; text-transform:uppercase; letter-spacing:1px; } + ul li { list-style-type:square; padding:1px 0; } ul.plainlist { margin-left:0 !important; } ul.plainlist li { list-style-type:none; } @@ -28,150 +24,83 @@ li ul { margin-bottom:0; } li, dt, dd { font-size:11px; line-height:14px; } dt { font-weight:bold; margin-top:4px; } dd { margin-left:0; } + form { margin:0; padding:0; } fieldset { margin:0; padding:0; } + blockquote { font-size:11px; color:#777; margin-left:2px; padding-left:10px; border-left:5px solid #ddd; } code, pre { font-family:"Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; background:inherit; color:#666; font-size:11px; } pre.literal-block { margin:10px; background:#eee; padding:6px 8px; } code strong { color:#930; } hr { clear:both; color:#eee; background-color:#eee; height:1px; border:none; margin:0; padding:0; font-size:1px; line-height:1px; } -/* PAGE STRUCTURE */ -#container { position:relative; width:100%; min-width:760px; } -#content { margin:10px 15px; } -#header { width:100%; } -#content-main { float:left; width:100%; } -#content-related { float:right; width:220px; position:relative; margin-right:-230px; } -#footer { clear:both; padding:10px; } - -/* COLUMN TYPES */ -.colMS { margin-right:245px !important; } -.colSM { margin-left:245px !important; } -.colSM #content-related { float:left; margin-right:0; margin-left:-230px; } -.colSM #content-main { float:right; } -.popup .colM { width:95%; } -.subcol { float:left; width:46%; margin-right:15px; } -.dashboard #content { width:500px; } - -/* HEADER */ -#header { background:#417690; color:#ffc; min-height:2.4em; overflow:hidden; } -#header a:link, #header a:visited { color:white; } -#header a:hover { text-decoration:underline; } -#branding h1 { padding:0.5em 10px 0 10px; font-size:18px; margin:0; font-weight:normal; color:#f4f379; } -#branding h2 { padding:0 10px 0.8em 10px; font-size:14px; margin:0; font-weight:normal; color:#ffc; } -#user-tools { position:absolute; top:0; right:0; padding:1.2em 10px; font-size:11px; text-align:right; } - -/* SIDEBAR */ -#content-related h3 { font-size:12px; color:#666; margin-bottom:3px; } -#content-related h4 { font-size:11px; } +/* TEXT STYLES & MODIFIERS */ +.small { font-size:11px; } +.tiny { font-size:10px; } +p.tiny { margin-top:-2px; } +.mini { font-size:9px; } +p.mini { margin-top:-3px; } +.help, p.help { font-size:10px !important; color:#999; } +p img, h1 img, h2 img, h3 img, h4 img, td img { vertical-align:middle; } +.quiet, a.quiet:link, a.quiet:visited { color:#999 !important;font-weight:normal !important; } +.quiet strong { font-weight:bold !important; } +.float-right { float:right; } +.float-left { float:left; } +.clear { clear:both; } +.align-left { text-align:left; } +.align-right { text-align:right; } +.example { margin:10px 0; padding:5px 10px; background:#efefef; } +.nowrap { white-space:nowrap; } -/* TABLES */ +/* TABLES */ table { border-collapse:collapse; border-color:#ccc; } td, th { font-size:11px; line-height:13px; border-bottom:1px solid #eee; vertical-align:top; padding:5px; font-family:"Lucida Grande", Verdana, Arial, sans-serif; } -th { text-align:left; font-size:12px; } -thead th { font-weight:bold; color:#666; padding:2px 5px; font-size:11px; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; border-left:1px solid #ddd; border-bottom:1px solid #ddd; } -thead th:first-child { border-left:none !important; } -.superwide table th, .superwide table td, .superwide table input, .superwide table select { font-size:10px; } -.module table { border-collapse: collapse; } +th { text-align:left; font-size:12px; font-weight:bold; } +thead th, +tfoot td { color:#666; padding:2px 5px; font-size:11px; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; border-left:1px solid #ddd; border-bottom:1px solid #ddd; } +tfoot td { border-bottom:none; border-top:1px solid #ddd; } +thead th:first-child, +tfoot td:first-child { border-left:none !important; } thead th.optional { font-weight:normal !important; } -#home-page table.module tr:hover { background:#EDF3FE; } fieldset table { border-right:1px solid #eee; } tr.row-label td { font-size:9px; padding-top:2px; padding-bottom:0; border-bottom:none; color:#666; margin-top:-1px; } tr.alt { background:#f6f6f6; } .row1 { background:#EDF3FE; } .row2 { background:white; } -table#change-history { width:100%; } -table#change-history tbody th { width:16em; } -/* TABLE SORTING */ +/* SORTABLE TABLES */ thead th a:link, thead th a:visited { color:#666; display:block; } table thead th.sorted { background-position:bottom left !important; } table thead th.sorted a { padding-right:13px; } table thead th.ascending a { background:url(../img/admin/arrow-down.gif) right .4em no-repeat; } table thead th.descending a { background:url(../img/admin/arrow-up.gif) right .4em no-repeat; } -/* MODULES */ -.module { border:1px solid #ccc; margin-bottom:5px; background:white; } -.module p, .module ul, .module h3, .module h4, .module dl, .module pre { padding-left:10px; padding-right:10px; } -.module blockquote { margin-left:12px; } -.module ul, .module ol { margin-left:1.5em; } -.module h2, .module caption { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; background:#7CA0C7 url(../img/admin/default-bg.gif) left top repeat-x; color:white; font-weight:bold; } -.module caption { border:1px solid #ccc; border-bottom:none; } -.module h3 { margin-top:.6em; } -#content-related .module h2 { background:#eee url(../img/admin/nav-bg.gif) bottom left repeat-x; color:#666; } -#content-main .verbose .actionlist { float:right; font-size:10px; width:17em; position:relative; top:-1.6em; margin:0 8px; } - -/* DASHBOARD */ -.dashboard .module table th { width:100%; } -.dashboard .module table td { white-space:nowrap; } -.dashboard .module table td a { display:block; padding-right:.6em; } +/* ORDERABLE TABLES */ +table.orderable tbody tr td:hover { cursor:move; } +table.orderable tbody tr td:first-child { padding-left:14px; background-image:url(../img/admin/nav-bg-grabber.gif); background-repeat:repeat-y; } +table.orderable-initalized .order-cell, body>tr>td.order-cell { display:none; } -/* RECENT ACTIONS MODULE */ -.module ul.actionlist { margin-left:0; } -ul.actionlist li { list-style-type:none; } - -/* FORM DEFAULTS */ -input, textarea, select { margin:2px 0; padding:2px 3px; vertical-align:middle; border:1px solid #ccc; font-family:"Lucida Grande", Verdana, Arial, sans-serif; font-weight:normal; font-size:11px; } +/* FORM DEFAULTS */ +input, textarea, select { margin:2px 0; padding:2px 3px; vertical-align:middle; font-family:"Lucida Grande", Verdana, Arial, sans-serif; font-weight:normal; font-size:11px; } textarea { vertical-align:top !important; } -input[type=checkbox], input[type=radio] { border:none; } +input[type=text], input[type=password], textarea, select, .vTextField { border:1px solid #ccc; } /* FORM BUTTONS */ input[type=submit], input[type=button], .submit-row input { background:white url(../img/admin/nav-bg.gif) bottom repeat-x; padding:3px; color:black; } input[type=submit]:active, input[type=button]:active { background-image:url(../img/admin/nav-bg-reverse.gif); background-position:top; } input[type=submit].default, .submit-row input.default { border:2px solid #5b80b2; background:#7CA0C7 url(../img/admin/default-bg.gif) bottom repeat-x; font-weight:bold; color:white; } input[type=submit].default:active { background-image:url(../img/admin/default-bg-reverse.gif); background-position:top; } -.submit-row { padding:5px 7px; text-align:right; background:#ffc; border:1px solid #ccc; margin:5px 0; } -.submit-row input { margin:0 0 0 5px; } -.submit-row .float-left { padding-top:.1em; } - -/* FORM ROWS */ -.form-row { clear:both; padding:8px 12px; font-size:11px; } -html>body .form-row { border-bottom:1px solid #eee; } -.form-row:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } -.form-row img, .form-row input { vertical-align:middle; } -form .form-row p { padding-left:0; font-size:11px; } - -/* FORM LABELS */ -form h4 { margin:0 !important; padding:0 !important; border:none !important; } -label { font-weight:normal !important; color:#666; font-size:12px; } -label.inline { margin-left:20px; } -.required label, label.required { font-weight:bold !important; color:#333 !important; } - -/* RADIO BUTTONS */ -form ul.radiolist li { list-style-type:none; } -form ul.radiolist label { float:none; display:inline; } -form ul.inline { margin-left:0; padding:0; } -form ul.inline li { float:left; padding-right:7px; } - -/* ALIGNED FIELDSETS */ -.aligned label { display:block; padding:0 1em 3px 0; float:left; text-align:left; width:8em; } -.aligned label.inline { display:inline; float:none; } -.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { width:350px; } -form .aligned p, form .aligned ul { margin-left:7em; padding-left:30px; } -form .aligned table p { margin-left:0; padding-left:0; } -form .aligned p.help { padding-left:38px; } -.aligned .vCheckboxLabel { float:none !important; display:inline; } -.colM .aligned .vLargeTextField, colM .aligned .vXMLLargeTextField { width:610px; } -.checkbox-row p.help { margin-left:0; padding-left:0 !important; } - -/* WIDE FIELDSETS */ -.wide label { width:15em !important; } -form .wide p { margin-left:15em; } -form .wide p.help { padding-left:38px; } -.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { width:450px; } -/* COLLAPSED FIELDSETS */ -fieldset.collapsed * { display:none; } -fieldset.collapsed h2, fieldset.collapsed { display:block !important; } -fieldset.collapsed .collapse-toggle { display: inline !important; } -fieldset.collapse h2 a.collapse-toggle { color:#ffc; } -fieldset.collapse h2 a.collapse-toggle:hover { text-decoration:underline; } -.hidden { display:none; } - -/* MONOSPACE TEXTAREAS */ -fieldset.monospace textarea { font-family:"Bitstream Vera Sans Mono",Monaco,"Courier New",Courier,monospace; } +/* MODULES */ +.module { border:1px solid #ccc; margin-bottom:5px; background:white; } +.module p, .module ul, .module h3, .module h4, .module dl, .module pre { padding-left:10px; padding-right:10px; } +.module blockquote { margin-left:12px; } +.module ul, .module ol { margin-left:1.5em; } +.module h3 { margin-top:.6em; } +.module h2, .module caption { margin:0; padding:2px 5px 3px 5px; font-size:11px; text-align:left; font-weight:bold; background:#7CA0C7 url(../img/admin/default-bg.gif) top left repeat-x; color:white; } +.module table { border-collapse: collapse; } -/* MESSAGES & ERRORS */ +/* MESSAGES & ERRORS */ ul.messagelist { padding:0 0 5px 0; margin:0; } ul.messagelist li { font-size:12px; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border-bottom:1px solid #ddd; color:#666; background:#ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; } .errornote { font-size:12px !important; display:block; padding:4px 5px 4px 25px; margin:0 0 3px 0; border:1px solid red; color:red;background:#ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; } @@ -183,17 +112,21 @@ td ul.errorlist li { margin:0 !important; } .error input, .error select { border:1px solid red; } div.system-message { background: #ffc; margin: 10px; padding: 6px 8px; font-size: .8em; } div.system-message p.system-message-title { padding:4px 5px 4px 25px; margin:0; color:red; background:#ffc url(../img/admin/icon_error.gif) 5px .3em no-repeat; } +.description { font-size:12px; padding:5px 0 0 12px; } -/* ACTION ICONS */ +/* BREADCRUMBS */ +div.breadcrumbs { background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; padding:2px 8px 3px 8px; font-size:11px; color:#999; border-top:1px solid white; border-bottom:1px solid #ccc; text-align:left; } + +/* ACTION ICONS */ .addlink { padding-left:12px; background:url(../img/admin/icon_addlink.gif) 0 .2em no-repeat; } .changelink { padding-left:12px; background:url(../img/admin/icon_changelink.gif) 0 .2em no-repeat; } -.deletelink { padding-left:12px; background:url(../img/admin/icon_deletelink.gif) 0 .2em no-repeat; } +.deletelink { padding-left:12px; background:url(../img/admin/icon_deletelink.gif) 0 .25em no-repeat; } a.deletelink:link, a.deletelink:visited { color:#CC3434; } a.deletelink:hover { color:#993333; } -/* OBJECT TOOLS */ -.object-tools { font-size:10px; font-weight:bold; font-family:Arial,Helvetica,sans-serif; padding-left:0; margin-bottom:5px; float:right; position:relative; margin-top:-2.4em; margin-bottom:-2em; } -.form-row .object-tools { margin-top:0; margin-bottom:0; } +/* OBJECT TOOLS */ +.object-tools { font-size:10px; font-weight:bold; font-family:Arial,Helvetica,sans-serif; padding-left:0; float:right; position:relative; margin-top:-2.4em; margin-bottom:-2em; } +.form-row .object-tools { margin-top:5px; margin-bottom:5px; float:none; height:2em; padding-left:3.5em; } .object-tools li { display:block; float:left; background:url(../img/admin/tool-left.gif) 0 0 no-repeat; padding:0 0 0 8px; margin-left:2px; height:16px; } .object-tools li:hover { background:url(../img/admin/tool-left_over.gif) 0 0 no-repeat; } .object-tools a:link, .object-tools a:visited { display:block; float:left; color:white; padding:.1em 14px .1em 8px; height:14px; background:#999 url(../img/admin/tool-right.gif) 100% 0 no-repeat; } @@ -203,123 +136,6 @@ a.deletelink:hover { color:#993333; } .object-tools a.addlink { background:#999 url(../img/admin/tooltag-add.gif) top right no-repeat; padding-right:28px; } .object-tools a.addlink:hover { background:#5b80b2 url(../img/admin/tooltag-add_over.gif) top right no-repeat; } -/* INLINE CONTROLS */ -#inline-controls { font-weight:bold; font-size:12px; } -#inline-specific-controls { margin-left:6px; padding:0 8px; border-left:6px solid #ccc; } - -/* BREADCRUMBS */ -p.breadcrumbs { font-size:11px; color:#ccc;text-align:left; } /* old breadcrumbs style */ -div.breadcrumbs { background:white url(../img/admin/nav-bg-reverse.gif) 0 -10px repeat-x; padding:2px 8px 3px 8px; font-size:11px; color:#999; border-top:1px solid white; border-bottom:1px solid #ccc; text-align:left; } - -/* SELECTOR (FILTER INTERFACE) */ -.selector { width:580px; float:left; } -.selector select { width:270px; height:170px; } -.selector-available, .selector-chosen { float:left; width:270px; text-align:center; margin-bottom:5px; } -.selector-available h2, .selector-chosen h2 { border:1px solid #ccc; } -.selector .selector-available h2 { background:white url(../img/admin/nav-bg.gif) bottom left repeat-x; color:#666; } -.selector .selector-filter { background:white; border:1px solid #ccc; border-width:0 1px; padding:3px; color:#999; font-size:10px; margin:0; text-align:left; } -.selector .selector-chosen .selector-filter { padding:4px 5px; } -.selector .selector-available input { width:230px; } -.selector ul.selector-chooser { float:left; width:22px; height:50px; background:url(../img/admin/chooser-bg.gif) top center no-repeat; margin:13% 3px 0 3px; padding:0; } -.selector-chooser li { margin:0; padding:3px; list-style-type:none; } -.selector select { margin-bottom:5px; margin-top:0; } -.selector-add, .selector-remove { width:16px; height:16px; display:block; text-indent:-3000px; } -.selector-add { background:url(../img/admin/selector-add.gif) top center no-repeat; margin-bottom:2px; } -.selector-remove { background:url(../img/admin/selector-remove.gif) top center no-repeat; } -a.selector-chooseall, a.selector-clearall { display:block; width:6em; text-align:left; margin-left:auto; margin-right:auto; font-weight:bold; color:#666; padding:3px 0 3px 18px; } -a.selector-chooseall:hover, a.selector-clearall:hover { color:#036; } -a.selector-chooseall { width:7em; background:url(../img/admin/selector-addall.gif) left center no-repeat; } -a.selector-clearall { background:url(../img/admin/selector-removeall.gif) left center no-repeat; } - -/* Stacked selectors for long items */ -.stacked { float:left; width:500px; } -.stacked select { width:480px; height:100px; } -.stacked .selector-available, .stacked .selector-chosen { width:480px; } -.stacked .selector-available { margin-bottom:0; } -.stacked .selector-available input { width:442px; } -.stacked ul.selector-chooser { height:22px; width:50px; margin:0 0 3px 40%; background:url(../img/admin/chooser_stacked-bg.gif) top center no-repeat; } -.stacked .selector-chooser li { float:left; padding:3px 3px 3px 5px; } -.stacked .selector-chooseall, .stacked .selector-clearall { display:none; } -.stacked .selector-add { background-image:url(../img/admin/selector_stacked-add.gif); } -.stacked .selector-remove { background-image:url(../img/admin/selector_stacked-remove.gif); } - -/* DATE AND TIME */ -p.datetime { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } -.datetime span { font-size:11px; font-weight:normal; color:#ccc; white-space:nowrap; } -.vDateField { margin-left:4px; } -table p.datetime { font-size:10px; margin-left:0; padding-left:0; } - -/* FILE UPLOADS */ -p.file-upload { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } -.file-upload a { font-weight:normal; } -.file-upload .deletelink { margin-left:5px; } - -/* CALENDARS & CLOCKS */ -.calendarbox, .clockbox { margin:5px auto; font-size:11px; width: 16em; text-align: center; background:white; position:relative; } -.clockbox { width:9em; } -.calendar { margin:0; padding: 0; } -.calendar table { margin: 0; padding: 0; border-collapse:collapse; background:white; width:99%; } -.calendar caption, .calendarbox h2 { margin: 0; font-size:11px; text-align:center; border-top:none; } -.calendar th { font-size:10px; color:#666; padding:2px 3px; text-align:center; background:#e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; border-bottom:1px solid #ddd; } -.calendar td { font-size:11px; text-align: center; padding: 0; border-top:1px solid #eee; border-bottom:none; } -.calendar td.selected a { background: #C9DBED; } -.calendar td.nonday { background:#efefef; } -.calendar td.today a { background:#ffc; } -.calendar td a, .timelist a { display: block; font-weight:bold; padding:4px; text-decoration: none; color:#444; } -.calendar td a:hover, .timelist a:hover { background: #5b80b2; color:white; } -.calendar td a:active, .timelist a:active { background: #036; color:white; } -.calendarnav { font-size:10px; text-align: center; color:#ccc; margin:0; padding:1px 3px; } -.calendarnav a:link, #calendarnav a:visited, #calendarnav a:hover { color: #999; } -.calendar-shortcuts { background:white; font-size:10px; line-height:11px; border-top:1px solid #eee; padding:3px 0 4px; color:#ccc; } -.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { display:block; position:absolute; font-weight:bold; font-size:12px; background:#C9DBED url(../img/admin/default-bg.gif) bottom left repeat-x; padding:1px 4px 2px 4px; color:white; } -.calendarnav-previous:hover, .calendarnav-next:hover { background:#036; } -.calendarnav-previous { top:0; left:0; } -.calendarnav-next { top:0; right:0; } -.calendar-cancel { margin:0 !important; padding:0; font-size:10px; background:#e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; border-top:1px solid #ddd; } -.calendar-cancel a { padding:2px; color:#999; } -ul.timelist, .timelist li { list-style-type:none; margin:0; padding:0; } -.timelist a { padding:2px; } - -/* ORDERING WIDGET */ -ul#orderthese { position:absolute; top:8em; right:0; width:240px; padding:0; margin:0; list-style-type:none; } -ul#orderthese li { list-style-type:none; display:block; padding:0; margin:6px 0; width:214px; background:#f6f6f6; white-space:nowrap; overflow:hidden; } -ul#orderthese li span { display:block; border:1px solid #e7e7e7; background:transparent url(../img/admin/nav-bg-grabber.gif) top left repeat-y; font-size:10px !important; padding:4px 6px 4px 12px; } -ul#orderthese span:hover { background-color:#efefef; } - -/* PAGINATOR */ -.paginator { font-size:11px; padding-top:10px; padding-bottom:10px; line-height:22px; margin:0; border-top:1px solid #ddd; } -.paginator a:link, .paginator a:visited { padding:2px 6px; border:solid 1px #ccc; background:white; text-decoration:none; } -.paginator a.showall { padding:0 !important; border:none !important; } -.paginator a.showall:hover { color:#036 !important; background:transparent !important; } -.paginator .end { border-width:2px !important; margin-right:6px; } -.paginator .this-page { padding:2px 6px; font-weight:bold; font-size:13px; vertical-align:top; } -.paginator a:hover { color:white; background:#5b80b2; border-color:#036; } - -/* TEXT STYLES & MODIFIERS */ -.small { font-size:11px; } -.tiny { font-size:10px; } -p.tiny { margin-top:-2px; } -.mini { font-size:9px; } -p.mini { margin-top:-3px; } -.help, p.help { font-size:10px !important; color:#999; } -p img, h1 img, h2 img, h3 img, h4 img, td img { vertical-align:middle; } -.quiet, a.quiet:link, a.quiet:visited { color:#999 !important;font-weight:normal !important; } -.quiet strong { font-weight:bold !important; } -.float-right { float:right; } -.float-left { float:left; } -.clear { clear:both; } -.align-left { text-align:left; } -.align-right { text-align:right; } -.example { margin:10px 0; padding:5px 10px; background:#efefef; } -.nowrap { white-space:nowrap; } - -/* CUSTOM FORM FIELDS */ -.vSelectMultipleField { vertical-align:top !important; } -.vCheckboxField { border:none; } -.vDateField, .vTimeField { margin-right:2px; } -.vFileUploadField { border:none; } -.vURLField { width:380px; } -.vLargeTextField, .vXMLLargeTextField { width:480px; } -.colM .vLargeTextField, .colM .vXMLLargeTextField { width:720px; } -body.core-flatfile #id_content { height: 400px; } -.module table .vPositiveSmallIntegerField { width: 22px; }
\ No newline at end of file +/* OBJECT HISTORY */ +table#change-history { width:100%; } +table#change-history tbody th { width:16em; } diff --git a/django/contrib/admin/media/css/layout.css b/django/contrib/admin/media/css/layout.css new file mode 100644 index 0000000000..19c9286b85 --- /dev/null +++ b/django/contrib/admin/media/css/layout.css @@ -0,0 +1,29 @@ +/* PAGE STRUCTURE */ +#container { position:relative; width:100%; min-width:760px; } +#content { margin:10px 15px; } +#header { width:100%; } +#content-main { float:left; width:100%; } +#content-related { float:right; width:220px; position:relative; margin-right:-230px; } +#footer { clear:both; padding:10px; } + +/* COLUMN TYPES */ +.colMS { margin-right:245px !important; } +.colSM { margin-left:245px !important; } +.colSM #content-related { float:left; margin-right:0; margin-left:-230px; } +.colSM #content-main { float:right; } +.popup .colM { width:95%; } +.subcol { float:left; width:46%; margin-right:15px; } +.dashboard #content { width:500px; } + +/* HEADER */ +#header { background:#417690; color:#ffc; overflow:hidden; } +#header a:link, #header a:visited { color:white; } +#header a:hover { text-decoration:underline; } +#branding h1 { padding:0 10px; font-size:18px; margin:8px 0; font-weight:normal; color:#f4f379; } +#branding h2 { padding:0 10px; font-size:14px; margin:-8px 0 8px 0; font-weight:normal; color:#ffc; } +#user-tools { position:absolute; top:0; right:0; padding:1.2em 10px; font-size:11px; text-align:right; } + +/* SIDEBAR */ +#content-related h3 { font-size:12px; color:#666; margin-bottom:3px; } +#content-related h4 { font-size:11px; } +#content-related .module h2 { background:#eee url(../img/admin/nav-bg.gif) bottom left repeat-x; color:#666; }
\ No newline at end of file diff --git a/django/contrib/admin/media/css/login.css b/django/contrib/admin/media/css/login.css new file mode 100644 index 0000000000..041135f31e --- /dev/null +++ b/django/contrib/admin/media/css/login.css @@ -0,0 +1,13 @@ +@import url('base.css'); +@import url('layout.css'); + +/* LOGIN FORM */ +body.login { background:#eee; } +.login #container { background:white; border:1px solid #ccc; width:28em; min-width:300px; margin-left:auto; margin-right:auto; margin-top:100px; } +.login #content-main { width:100%; } +.login form { margin-top:1em; } +.login .form-row { padding:4px 0; float:left; width:100%; } +.login .form-row label { float:left; width:7em; padding-right:0.5em; line-height:2em; text-align:right; font-size:1em; color:#333; } +.login .form-row #id_username, .login .form-row #id_password { width:16em; } +.login span.help { font-size:10px; display:block; } +.login .submit-row { clear:both; padding:1em 0 0 7.4em; }
\ No newline at end of file diff --git a/django/contrib/admin/media/css/patch-iewin.css b/django/contrib/admin/media/css/patch-iewin.css index 46531325df..b2e6a4c560 100644 --- a/django/contrib/admin/media/css/patch-iewin.css +++ b/django/contrib/admin/media/css/patch-iewin.css @@ -1,7 +1,6 @@ * html #container { position:static; } /* keep header from flowing off the page */ * html .colMS #content-related { margin-right:0; margin-left:10px; position:static; } /* put the right sidebars back on the page */ * html .colSM #content-related { margin-right:10px; margin-left:-115px; position:static; } /* put the left sidebars back on the page */ +* html .form-row { height:1%; } * html .dashboard #content { width:768px; } /* proper fixed width for dashboard in IE6 */ -* html .dashboard #content-main { width:535px; } /* proper fixed width for dashboard in IE6 */ -* html #content { width /**/: 768px; } /* fixed width for IE5 */ -* html #content-main { width /**/: 535px; } /* fixed width for IE5 */
\ No newline at end of file +* html .dashboard #content-main { width:535px; } /* proper fixed width for dashboard in IE6 */
\ No newline at end of file diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css new file mode 100644 index 0000000000..bf526bfd66 --- /dev/null +++ b/django/contrib/admin/media/css/widgets.css @@ -0,0 +1,101 @@ +/* SELECTOR (FILTER INTERFACE) */ +.selector { width:580px; float:left; } +.selector select { width:270px; height:17.2em; } +.selector-available, .selector-chosen { float:left; width:270px; text-align:center; margin-bottom:5px; } +.selector-available h2, .selector-chosen h2 { border:1px solid #ccc; } +.selector .selector-available h2 { background:white url(../img/admin/nav-bg.gif) bottom left repeat-x; color:#666; } +.selector .selector-filter { background:white; border:1px solid #ccc; border-width:0 1px; padding:3px; color:#999; font-size:10px; margin:0; text-align:left; } +.selector .selector-chosen .selector-filter { padding:4px 5px; } +.selector .selector-available input { width:230px; } +.selector ul.selector-chooser { float:left; width:22px; height:50px; background:url(../img/admin/chooser-bg.gif) top center no-repeat; margin:8em 3px 0 3px; padding:0; } +.selector-chooser li { margin:0; padding:3px; list-style-type:none; } +.selector select { margin-bottom:5px; margin-top:0; } +.selector-add, .selector-remove { width:16px; height:16px; display:block; text-indent:-3000px; } +.selector-add { background:url(../img/admin/selector-add.gif) top center no-repeat; margin-bottom:2px; } +.selector-remove { background:url(../img/admin/selector-remove.gif) top center no-repeat; } +a.selector-chooseall, a.selector-clearall { display:block; width:6em; text-align:left; margin-left:auto; margin-right:auto; font-weight:bold; color:#666; padding:3px 0 3px 18px; } +a.selector-chooseall:hover, a.selector-clearall:hover { color:#036; } +a.selector-chooseall { width:7em; background:url(../img/admin/selector-addall.gif) left center no-repeat; } +a.selector-clearall { background:url(../img/admin/selector-removeall.gif) left center no-repeat; } + +/* STACKED SELECTORS */ +.stacked { float:left; width:500px; } +.stacked select { width:480px; height:10.1em; } +.stacked .selector-available, .stacked .selector-chosen { width:480px; } +.stacked .selector-available { margin-bottom:0; } +.stacked .selector-available input { width:442px; } +.stacked ul.selector-chooser { height:22px; width:50px; margin:0 0 3px 40%; background:url(../img/admin/chooser_stacked-bg.gif) top center no-repeat; } +.stacked .selector-chooser li { float:left; padding:3px 3px 3px 5px; } +.stacked .selector-chooseall, .stacked .selector-clearall { display:none; } +.stacked .selector-add { background-image:url(../img/admin/selector_stacked-add.gif); } +.stacked .selector-remove { background-image:url(../img/admin/selector_stacked-remove.gif); } + +/* DATE AND TIME */ +p.datetime { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } +.datetime span { font-size:11px; color:#ccc; font-weight:normal; white-space:nowrap; } +.vDateField { margin-left:4px; } +table p.datetime { font-size:10px; margin-left:0; padding-left:0; } + +/* FILE UPLOADS */ +p.file-upload { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; } +.file-upload a { font-weight:normal; } +.file-upload .deletelink { margin-left:5px; } + +/* CALENDARS & CLOCKS */ +.calendarbox, .clockbox { margin:5px auto; font-size:11px; width:16em; text-align:center; background:white; position:relative; } +.clockbox { width:9em; } +.calendar { margin:0; padding: 0; } +.calendar table { margin:0; padding:0; border-collapse:collapse; background:white; width:99%; } +.calendar caption, .calendarbox h2 { margin: 0; font-size:11px; text-align:center; border-top:none; } +.calendar th { font-size:10px; color:#666; padding:2px 3px; text-align:center; background:#e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; border-bottom:1px solid #ddd; } +.calendar td { font-size:11px; text-align: center; padding: 0; border-top:1px solid #eee; border-bottom:none; } +.calendar td.selected a { background: #C9DBED; } +.calendar td.nonday { background:#efefef; } +.calendar td.today a { background:#ffc; } +.calendar td a, .timelist a { display: block; font-weight:bold; padding:4px; text-decoration: none; color:#444; } +.calendar td a:hover, .timelist a:hover { background: #5b80b2; color:white; } +.calendar td a:active, .timelist a:active { background: #036; color:white; } +.calendarnav { font-size:10px; text-align: center; color:#ccc; margin:0; padding:1px 3px; } +.calendarnav a:link, #calendarnav a:visited, #calendarnav a:hover { color: #999; } +.calendar-shortcuts { background:white; font-size:10px; line-height:11px; border-top:1px solid #eee; padding:3px 0 4px; color:#ccc; } +.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { display:block; position:absolute; font-weight:bold; font-size:12px; background:#C9DBED url(../img/admin/default-bg.gif) bottom left repeat-x; padding:1px 4px 2px 4px; color:white; } +.calendarnav-previous:hover, .calendarnav-next:hover { background:#036; } +.calendarnav-previous { top:0; left:0; } +.calendarnav-next { top:0; right:0; } +.calendar-cancel { margin:0 !important; padding:0; font-size:10px; background:#e1e1e1 url(../img/admin/nav-bg.gif) 0 50% repeat-x; border-top:1px solid #ddd; } +.calendar-cancel a { padding:2px; color:#999; } +ul.timelist, .timelist li { list-style-type:none; margin:0; padding:0; } +.timelist a { padding:2px; } + +/* INLINE ORDERER */ +ul.orderer { position:relative; padding:0 !important; margin:0 !important; list-style-type:none; } +ul.orderer li { list-style-type:none; display:block; padding:0; margin:0; border:1px solid #bbb; border-width:0 1px 1px 0; white-space:nowrap; overflow:hidden; background:#e2e2e2 url(../img/admin/nav-bg-grabber.gif) repeat-y; } +ul.orderer li:hover { cursor:move; background-color:#ddd; } +ul.orderer li a.selector { margin-left:12px; overflow:hidden; width:83%; font-size:10px !important; padding:0.6em 0; } +ul.orderer li a:link, ul.orderer li a:visited { color:#333; } +ul.orderer li .inline-deletelink { position:absolute; right:4px; margin-top:0.6em; } +ul.orderer li.selected { background-color:#f8f8f8; border-right-color:#f8f8f8; } +ul.orderer li.deleted { background:#bbb url(../img/admin/deleted-overlay.gif); } +ul.orderer li.deleted a:link, ul.orderer li.deleted a:visited { color:#888; } +ul.orderer li.deleted .inline-deletelink { background-image:url(../img/admin/inline-restore.png); } +ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { cursor:default; } + +/* EDIT INLINE */ +.inline-deletelink { display:block; text-indent:-9999px; background:transparent url(../img/admin/inline-delete.png) no-repeat; width:15px; height:15px; margin:0.4em 0; border: 0px none; } +.inline-deletelink:hover { background-position:-15px 0; cursor:pointer; } +.editinline button.addlink { border: 0px none; color: #5b80b2; font-size: 100%; cursor: pointer; } +.editinline button.addlink:hover { color: #036; cursor: pointer; } +.editinline table .help { text-align:right; float:right; padding-left:2em; } +.editinline tfoot .addlink { white-space:nowrap; } +.editinline table thead th:last-child { border-left:none; } +.editinline tr.deleted { background:#ddd url(../img/admin/deleted-overlay.gif); } +.editinline tr.deleted .inline-deletelink { background-image:url(../img/admin/inline-restore.png); } +.editinline tr.deleted td:hover { cursor:default; } +.editinline tr.deleted td:first-child { background-image:none !important; } + +/* EDIT INLINE - STACKED */ +.editinline-stacked { min-width:758px; } +.editinline-stacked .inline-object { margin-left:210px; background:white; } +.editinline-stacked .inline-source { float:left; width:200px; background:#f8f8f8; } +.editinline-stacked .inline-splitter { float:left; width:9px; background:#f8f8f8 url(../img/admin/inline-splitter-bg.gif) 50% 50% no-repeat; border-right:1px solid #ccc; } +.editinline-stacked .controls { clear:both; background:#e1e1e1 url(../img/admin/nav-bg.gif) top left repeat-x; padding:3px 4px; font-size:11px; border-top:1px solid #ddd; }
\ No newline at end of file diff --git a/django/contrib/admin/media/img/admin/deleted-overlay.gif b/django/contrib/admin/media/img/admin/deleted-overlay.gif Binary files differnew file mode 100644 index 0000000000..dc3828fe06 --- /dev/null +++ b/django/contrib/admin/media/img/admin/deleted-overlay.gif diff --git a/django/contrib/admin/media/img/admin/inline-delete-8bit.png b/django/contrib/admin/media/img/admin/inline-delete-8bit.png Binary files differnew file mode 100644 index 0000000000..95caf59a8d --- /dev/null +++ b/django/contrib/admin/media/img/admin/inline-delete-8bit.png diff --git a/django/contrib/admin/media/img/admin/inline-delete.png b/django/contrib/admin/media/img/admin/inline-delete.png Binary files differnew file mode 100644 index 0000000000..d59bcd2444 --- /dev/null +++ b/django/contrib/admin/media/img/admin/inline-delete.png diff --git a/django/contrib/admin/media/img/admin/inline-restore-8bit.png b/django/contrib/admin/media/img/admin/inline-restore-8bit.png Binary files differnew file mode 100644 index 0000000000..e087c8ead3 --- /dev/null +++ b/django/contrib/admin/media/img/admin/inline-restore-8bit.png diff --git a/django/contrib/admin/media/img/admin/inline-restore.png b/django/contrib/admin/media/img/admin/inline-restore.png Binary files differnew file mode 100644 index 0000000000..efdd92ac39 --- /dev/null +++ b/django/contrib/admin/media/img/admin/inline-restore.png diff --git a/django/contrib/admin/media/img/admin/inline-splitter-bg.gif b/django/contrib/admin/media/img/admin/inline-splitter-bg.gif Binary files differnew file mode 100644 index 0000000000..32ac5b3498 --- /dev/null +++ b/django/contrib/admin/media/img/admin/inline-splitter-bg.gif diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js index 72c4e95eca..cb84790f44 100644 --- a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js +++ b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js @@ -39,7 +39,7 @@ function dismissAddAnotherPopup(win, newId, newRepr) { if (elem.nodeName == 'SELECT') { var o = new Option(newRepr, newId); elem.options[elem.options.length] = o; - elem.selectedIndex = elem.length - 1; + o.selected = true; } else if (elem.nodeName == 'INPUT') { elem.value = newId; } diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py new file mode 100644 index 0000000000..022d20bed9 --- /dev/null +++ b/django/contrib/admin/models.py @@ -0,0 +1,51 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ + +ADDITION = 1 +CHANGE = 2 +DELETION = 3 + +class LogEntryManager(models.Manager): + def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): + e = self.model(None, None, user_id, content_type_id, object_id, object_repr[:200], action_flag, change_message) + e.save() + +class LogEntry(models.Model): + action_time = models.DateTimeField(_('action time'), auto_now=True) + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType, blank=True, null=True) + object_id = models.TextField(_('object id'), blank=True, null=True) + object_repr = models.CharField(_('object repr'), maxlength=200) + action_flag = models.PositiveSmallIntegerField(_('action flag')) + change_message = models.TextField(_('change message'), blank=True) + objects = LogEntryManager() + class Meta: + verbose_name = _('log entry') + verbose_name_plural = _('log entries') + db_table = 'django_admin_log' + ordering = ('-action_time',) + + def __repr__(self): + return str(self.action_time) + + def is_addition(self): + return self.action_flag == ADDITION + + def is_change(self): + return self.action_flag == CHANGE + + def is_deletion(self): + return self.action_flag == DELETION + + def get_edited_object(self): + "Returns the edited object represented by this log entry" + return self.content_type.get_object_for_this_type(pk=self.object_id) + + def get_admin_url(self): + """ + Returns the admin URL to edit the object represented by this log entry. + This is relative to the Django admin index page. + """ + return "%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id) diff --git a/django/contrib/admin/models/__init__.py b/django/contrib/admin/models/__init__.py deleted file mode 100644 index e11e2df093..0000000000 --- a/django/contrib/admin/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['admin'] diff --git a/django/contrib/admin/models/admin.py b/django/contrib/admin/models/admin.py deleted file mode 100644 index b7bcda192b..0000000000 --- a/django/contrib/admin/models/admin.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.core import meta -from django.models import auth, core -from django.utils.translation import gettext_lazy as _ - -class LogEntry(meta.Model): - action_time = meta.DateTimeField(_('action time'), auto_now=True) - user = meta.ForeignKey(auth.User) - content_type = meta.ForeignKey(core.ContentType, blank=True, null=True) - object_id = meta.TextField(_('object id'), blank=True, null=True) - object_repr = meta.CharField(_('object repr'), maxlength=200) - action_flag = meta.PositiveSmallIntegerField(_('action flag')) - change_message = meta.TextField(_('change message'), blank=True) - class META: - module_name = 'log' - verbose_name = _('log entry') - verbose_name_plural = _('log entries') - db_table = 'django_admin_log' - ordering = ('-action_time',) - module_constants = { - 'ADDITION': 1, - 'CHANGE': 2, - 'DELETION': 3, - } - - def __repr__(self): - return str(self.action_time) - - def is_addition(self): - return self.action_flag == ADDITION - - def is_change(self): - return self.action_flag == CHANGE - - def is_deletion(self): - return self.action_flag == DELETION - - def get_edited_object(self): - "Returns the edited object represented by this log entry" - return self.get_content_type().get_object_for_this_type(pk=self.object_id) - - def get_admin_url(self): - """ - Returns the admin URL to edit the object represented by this log entry. - This is relative to the Django admin index page. - """ - return "%s/%s/%s/" % (self.get_content_type().get_package(), self.get_content_type().python_module_name, self.object_id) - - def _module_log_action(user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): - e = LogEntry(None, None, user_id, content_type_id, object_id, object_repr[:200], action_flag, change_message) - e.save() diff --git a/django/contrib/admin/templates/admin/404.html b/django/contrib/admin/templates/admin/404.html index d791f565ba..9bf4293e76 100644 --- a/django/contrib/admin/templates/admin/404.html +++ b/django/contrib/admin/templates/admin/404.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block title %}{% trans 'Page not found' %}{% endblock %} diff --git a/django/contrib/admin/templates/admin/500.html b/django/contrib/admin/templates/admin/500.html index 9d3e3de32c..b30e43170d 100644 --- a/django/contrib/admin/templates/admin/500.html +++ b/django/contrib/admin/templates/admin/500.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="/">{% trans "Home" %}</a> › {% trans "Server error" %}</div>{% endblock %} diff --git a/django/contrib/admin/templates/admin/base_site.html b/django/contrib/admin/templates/admin/base_site.html index b4d285b779..b867bd29bd 100644 --- a/django/contrib/admin/templates/admin/base_site.html +++ b/django/contrib/admin/templates/admin/base_site.html @@ -1,4 +1,4 @@ -{% extends "admin/base" %} +{% extends "admin/base.html" %} {% load i18n %} {% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 02d5760509..a667087f5f 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -1,36 +1,39 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n admin_modify adminmedia %} {% block extrahead %}{{ block.super }} <script type="text/javascript" src="../../../jsi18n/"></script> -{% for js in bound_manipulator.javascript_imports %}{% include_admin_script js %}{% endfor %} +{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %} {% endblock %} -{% block coltype %}{{ bound_manipulator.coltype }}{% endblock %} -{% block bodyclass %}{{ app_label }}-{{ bound_manipulator.object_name.lower }} change-form{% endblock %} +{% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %} +{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %} +{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} {% block userlinks %}<a href="../../../doc/">{% trans 'Documentation' %}</a> / <a href="../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block breadcrumbs %}{% if not is_popup %} <div class="breadcrumbs"> <a href="../../../">{% trans "Home" %}</a> › - <a href="../">{{ bound_manipulator.verbose_name_plural|capfirst }}</a> › - {% if add %}{% trans "Add" %} {{ bound_manipulator.verbose_name }}{% else %}{{ bound_manipulator.original|striptags|truncatewords:"18" }}{% endif %} + <a href="../">{{ opts.verbose_name_plural|capfirst }}</a> › + {% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|striptags|truncatewords:"18" }}{% endif %} </div> {% endif %}{% endblock %} {% block content %}<div id="content-main"> {% if change %}{% if not is_popup %} <ul class="object-tools"><li><a href="history/" class="historylink">{% trans "History" %}</a></li> - {% if bound_manipulator.has_absolute_url %}<li><a href="/r/{{ bound_manipulator.content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%} + {% if has_absolute_url %}<li><a href="../../../r/{{ content_type_id }}/{{ object_id }}/" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%} </ul> {% endif %}{% endif %} -<form {{ bound_manipulator.form_enc_attrib }} action="{{ form_url }}" method="post">{% block form_top %}{% endblock %} +<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post">{% block form_top %}{% endblock %} +<div> {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %} -{% if bound_manipulator.save_on_top %}{% submit_row bound_manipulator %}{% endif %} +{% if opts.admin.save_on_top %}{% submit_row %}{% endif %} {% if form.error_dict %} <p class="errornote"> {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} </p> {% endif %} -{% for bound_field_set in bound_manipulator.bound_field_sets %} +{% for bound_field_set in bound_field_sets %} <fieldset class="module aligned {{ bound_field_set.classes }}"> {% if bound_field_set.name %}<h2>{{ bound_field_set.name }}</h2>{% endif %} + {% if bound_field_set.description %}<div class="description">{{ bound_field_set.description }}</div>{% endif %} {% for bound_field_line in bound_field_set %} {% admin_field_line bound_field_line %} {% for bound_field in bound_field_line %} @@ -41,7 +44,7 @@ {% endfor %} {% block after_field_sets %}{% endblock %} {% if change %} - {% if bound_manipulator.ordered_objects %} + {% if ordered_objects %} <fieldset class="module"><h2>{% trans "Ordering" %}</h2> <div class="form-row{% if form.order_.errors %} error{% endif %} "> {% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %} @@ -49,27 +52,17 @@ </div></fieldset> {% endif %} {% endif %} -{% for related_object in bound_manipulator.inline_related_objects %}{% edit_inline related_object %}{% endfor %} +{% for related_object in inline_related_objects %}{% edit_inline related_object %}{% endfor %} {% block after_related_objects %}{% endblock %} -{% submit_row bound_manipulator %} +{% submit_row %} {% if add %} - <script type="text/javascript">document.getElementById("{{ bound_manipulator.first_form_field_id }}").focus();</script> + <script type="text/javascript">document.getElementById("{{ first_form_field_id }}").focus();</script> {% endif %} -{% if bound_manipulator.auto_populated_fields %} +{% if auto_populated_fields %} <script type="text/javascript"> - {% auto_populated_field_script bound_manipulator.auto_populated_fields change %} + {% auto_populated_field_script auto_populated_fields change %} </script> {% endif %} -{% if change %} - {% if bound_manipulator.ordered_objects %} - {% if form.order_objects %}<ul id="orderthese"> - {% for object in form.order_objects %} - <li id="p{% object_pk bound_manipulator object %}"> - <span id="handlep{% object_pk bound_manipulator object %}">{{ object|truncatewords:"5" }}</span> - </li> - {% endfor %} - </ul>{% endif %} - {% endif %} -{% endif %} +</div> </form></div> {% endblock %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 17b388b699..5b54bfb8cc 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -1,8 +1,9 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load adminmedia admin_list i18n %} +{% block stylesheet %}{% admin_media_prefix %}css/changelists.css{% endblock %} {% block bodyclass %}change-list{% endblock %} {% block userlinks %}<a href="../../doc/">{% trans 'Documentation' %}</a> / <a href="../../password_change/">{% trans 'Change password' %}</a> / <a href="../../logout/">{% trans 'Log out' %}</a>{% endblock %} -{% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> › {{ cl.opts.verbose_name_plural|capfirst }} </div>{% endblock %}{% endif %} +{% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> › {{ cl.opts.verbose_name_plural|capfirst }}</div>{% endblock %}{% endif %} {% block coltype %}flex{% endblock %} {% block content %} <div id="content-main"> diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 7fba4cebd7..f907c18a16 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -1,6 +1,14 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block userlinks %}<a href="../../../../doc/">{% trans 'Documentation' %}</a> / <a href="../../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../../logout/">{% trans 'Log out' %}</a>{% endblock %} +{% block breadcrumbs %} +<div class="breadcrumbs"> + <a href="../../../../">{% trans "Home" %}</a> › + <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> › + <a href="../">{{ object|striptags|truncatewords:"18" }}</a> › + {% trans 'Delete' %} +</div> +{% endblock %} {% block content %} {% if perms_lacking %} <p>{% blocktrans %}Deleting the {{ object_name }} '{{ object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p> @@ -13,8 +21,10 @@ <p>{% blocktrans %}Are you sure you want to delete the {{ object_name }} "{{ object }}"? All of the following related items will be deleted:{% endblocktrans %}</p> <ul>{{ deleted_objects|unordered_list }}</ul> <form action="" method="post"> + <div> <input type="hidden" name="post" value="yes" /> <input type="submit" value="{% trans "Yes, I'm sure" %}" /> + </div> </form> {% endif %} {% endblock %} diff --git a/django/contrib/admin/templates/admin/edit_inline_stacked.html b/django/contrib/admin/templates/admin/edit_inline_stacked.html index 62549ef82d..45aa0a4f58 100644 --- a/django/contrib/admin/templates/admin/edit_inline_stacked.html +++ b/django/contrib/admin/templates/admin/edit_inline_stacked.html @@ -13,4 +13,4 @@ {% endif %} {% endfor %} {% endfor %} -</fieldset>
\ No newline at end of file +</fieldset> diff --git a/django/contrib/admin/templates/admin/field_line.html b/django/contrib/admin/templates/admin/field_line.html index 10f37d5dc9..b7e2fc2ae0 100644 --- a/django/contrib/admin/templates/admin/field_line.html +++ b/django/contrib/admin/templates/admin/field_line.html @@ -9,14 +9,6 @@ {% if not bound_field.has_label_first %} {% field_label bound_field %} {% endif %} - {% if change %} - {% if bound_field.field.primary_key %} - {{ bound_field.original_value }} - {% endif %} - {% if bound_field.raw_id_admin %} - {% if bound_field.existing_display %} <strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %} - {% endif %} - {% endif %} {% if bound_field.field.help_text %}<p class="help">{{ bound_field.field.help_text }}</p>{% endif %} {% endfor %} </div> diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html index aabd4eecca..246086861b 100644 --- a/django/contrib/admin/templates/admin/index.html +++ b/django/contrib/admin/templates/admin/index.html @@ -1,6 +1,7 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} +{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/dashboard.css{% endblock %} {% block coltype %}colMS{% endblock %} {% block bodyclass %}dashboard{% endblock %} {% block breadcrumbs %}{% endblock %} @@ -13,14 +14,14 @@ {% if app_list %} {% for app in app_list %} <div class="module"> - <h2>{{ app.name }}</h2> - <table> + <table summary="{% blocktrans with app.name as name %}Models available in the {{ name }} application.{% endblocktrans %}"> + <caption>{{ app.name }}</caption> {% for model in app.models %} <tr> {% if model.perms.change %} - <th><a href="{{ model.admin_url }}">{{ model.name }}</a></th> + <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th> {% else %} - <th>{{ model.name }}</th> + <th scope="row">{{ model.name }}</th> {% endif %} {% if model.perms.add %} @@ -57,7 +58,7 @@ {% else %} <ul class="actionlist"> {% for entry in admin_log %} - <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">{% if not entry.is_deletion %}<a href="{{ entry.get_admin_url }}">{% endif %}{{ entry.object_repr|escape }}{% if not entry.is_deletion %}</a>{% endif %}<br /><span class="mini quiet">{{ entry.get_content_type.name|capfirst }}</span></li> + <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">{% if not entry.is_deletion %}<a href="{{ entry.get_admin_url }}">{% endif %}{{ entry.object_repr|escape }}{% if not entry.is_deletion %}</a>{% endif %}<br /><span class="mini quiet">{{ entry.content_type.name|capfirst }}</span></li> {% endfor %} </ul> {% endif %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index ea823e1020..5f338f703e 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -1,6 +1,9 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} +{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/login.css{% endblock %} +{% block bodyclass %}login{% endblock %} +{% block content_title %}{% endblock %} {% block breadcrumbs %}{% endblock %} {% block content %} @@ -9,20 +12,18 @@ <p class="errornote">{{ error_message }}</p> {% endif %} <div id="content-main"> -<form action="{{ app_path }}" method="post"> - -<p class="aligned"> -<label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" /> -</p> -<p class="aligned"> -<label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" /> -<input type="hidden" name="this_is_the_login_form" value="1" /> -<input type="hidden" name="post_data" value="{{ post_data }}" />{% comment %} <span class="help">{% trans 'Have you <a href="/password_reset/">forgotten your password</a>?' %}</span>{% endcomment %} -</p> - -<div class="aligned "> -<label> </label><input type="submit" value="{% trans 'Log in' %}" /> -</div> +<form action="{{ app_path }}" method="post" id="login-form"> + <div class="form-row"> + <label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" /> + </div> + <div class="form-row"> + <label for="id_password">{% trans 'Password:' %}</label> <input type="password" name="password" id="id_password" /> + <input type="hidden" name="this_is_the_login_form" value="1" /> + <input type="hidden" name="post_data" value="{{ post_data }}" /> {% comment %}<span class="help">{% trans 'Have you <a href="/password_reset/">forgotten your password</a>?' %}</span>{% endcomment %} + </div> + <div class="submit-row"> + <label> </label><input type="submit" value="{% trans 'Log in' %}" /> + </div> </form> <script type="text/javascript"> diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html index b182f40acb..0dbe7af743 100644 --- a/django/contrib/admin/templates/admin/object_history.html +++ b/django/contrib/admin/templates/admin/object_history.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block userlinks %}<a href="../../../../doc/">{% trans 'Documentation' %}</a> / <a href="../../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block breadcrumbs %} @@ -15,16 +15,16 @@ <table id="change-history"> <thead> <tr> - <th>{% trans 'Date/time' %}</th> - <th>{% trans 'User' %}</th> - <th>{% trans 'Action' %}</th> + <th scope="col">{% trans 'Date/time' %}</th> + <th scope="col">{% trans 'User' %}</th> + <th scope="col">{% trans 'Action' %}</th> </tr> </thead> <tbody> {% for action in action_list %} <tr> - <th>{{ action.action_time|date:_("DATE_WITH_TIME_FULL") }}</th> - <td>{{ action.get_user.username }}{% if action.get_user.first_name %} ({{ action.get_user.first_name }} {{ action.get_user.last_name }}){% endif %}</td> + <th scope="row">{{ action.action_time|date:_("DATE_WITH_TIME_FULL") }}</th> + <td>{{ action.user.username }}{% if action.user.first_name %} ({{ action.user.first_name }} {{ action.user.last_name }}){% endif %}</td> <td>{{ action.change_message}}</td> </tr> {% endfor %} diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html index c2d6cf59c5..24eced9ef9 100644 --- a/django/contrib/admin/templates/admin/search_form.html +++ b/django/contrib/admin/templates/admin/search_form.html @@ -3,7 +3,7 @@ {% if cl.lookup_opts.admin.search_fields %} <div id="toolbar"><form id="changelist-search" action="" method="get"> <div><!-- DIV needed for valid HTML --> -<label><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" alt="Search" /></label> +<label for="searchbar"><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" alt="Search" /></label> <input type="text" size="40" name="{{ search_var }}" value="{{ cl.query|escape }}" id="searchbar" /> <input type="submit" value="{% trans 'Go' %}" /> {% if show_result_count %} diff --git a/django/contrib/admin/templates/admin/template_validator.html b/django/contrib/admin/templates/admin/template_validator.html index f9ac09a77d..422e90261f 100644 --- a/django/contrib/admin/templates/admin/template_validator.html +++ b/django/contrib/admin/templates/admin/template_validator.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% block content %} diff --git a/django/contrib/admin/templates/admin_doc/bookmarklets.html b/django/contrib/admin/templates/admin_doc/bookmarklets.html index 708473750a..fa5942926f 100644 --- a/django/contrib/admin/templates/admin_doc/bookmarklets.html +++ b/django/contrib/admin/templates/admin_doc/bookmarklets.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% block breadcrumbs %}{% load i18n %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> › <a href="../">{% trans "Documentation" %}</a> › {% trans "Bookmarklets" %}</div>{% endblock %} {% block userlinks %}<a href="../../password_change/">{% trans 'Change password' %}</a> / <a href="../../logout/">{% trans 'Log out' %}</a>{% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/index.html b/django/contrib/admin/templates/admin_doc/index.html index 6665f4bb42..331774d2b3 100644 --- a/django/contrib/admin/templates/admin_doc/index.html +++ b/django/contrib/admin/templates/admin_doc/index.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %}
+{% extends "admin/base_site.html" %}
{% load i18n %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">Home</a> › Documentation</div>{% endblock %}
{% block userlinks %}<a href="../password_change/">{% trans 'Change password' %}</a> / <a href="../logout/">{% trans 'Log out' %}</a>{% endblock %}
diff --git a/django/contrib/admin/templates/admin_doc/missing_docutils.html b/django/contrib/admin/templates/admin_doc/missing_docutils.html index a7cf7e95d7..d0b571f957 100644 --- a/django/contrib/admin/templates/admin_doc/missing_docutils.html +++ b/django/contrib/admin/templates/admin_doc/missing_docutils.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">Home</a> › Documentation</div>{% endblock %} {% block userlinks %}<a href="../password_change/">{% trans 'Change password' %}</a> / <a href="../logout/">{% trans 'Log out' %}</a>{% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/model_detail.html b/django/contrib/admin/templates/admin_doc/model_detail.html index 0eb2971048..953bb4bf54 100644 --- a/django/contrib/admin/templates/admin_doc/model_detail.html +++ b/django/contrib/admin/templates/admin_doc/model_detail.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block userlinks %}<a href="../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block extrahead %} @@ -41,6 +41,6 @@ </table> </div> -<p class="small"><a href="../">‹ Back to Models Documentation</p> +<p class="small"><a href="../">‹ Back to Models Documentation</a></p> </div> {% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/model_index.html b/django/contrib/admin/templates/admin_doc/model_index.html index 3183b577e1..c681da75c9 100644 --- a/django/contrib/admin/templates/admin_doc/model_index.html +++ b/django/contrib/admin/templates/admin_doc/model_index.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block coltype %}colSM{% endblock %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">Home</a> › <a href="../">Documentation</a> › Models</div>{% endblock %} @@ -8,18 +8,19 @@ {% block content %} -<h1>Models Documentation</h1> +<h1>Model documentation</h1> + +{% regroup models by app_label as grouped_models %} <div id="content-main"> -{% regroup models|dictsort:"module" by module as grouped_models %} {% for group in grouped_models %} <div class="module"> -<h2 id='{{ group.grouper }}'>{{ group.grouper }}</h2> +<h2 id="{{ group.grouper }}">{{ group.grouper|capfirst }}</h2> <table class="xfull"> {% for model in group.list %} <tr> -<th><a href="{{ model.name }}/">{{ model.class }}</a></th> +<th><a href="{{ model.app_label }}.{{ model.object_name.lower }}/">{{ model.object_name }}</a></th> </tr> {% endfor %} </table> @@ -32,11 +33,11 @@ {% block sidebar %} <div id="content-related" class="sidebar"> <div class="module"> -<h2>Model Groups Quick List</h2> +<h2>Model groups</h2> <ul> -{% regroup models|dictsort:"module" by module as grouped_models %} +{% regroup models by app_label as grouped_models %} {% for group in grouped_models %} - <li><a href="#{{ group.grouper }}">{{ group.grouper }}</a></li> + <li><a href="#{{ group.grouper }}">{{ group.grouper|capfirst }}</a></li> {% endfor %} </ul> </div> diff --git a/django/contrib/admin/templates/admin_doc/template_detail.html b/django/contrib/admin/templates/admin_doc/template_detail.html index d2a2831098..df67f1856b 100644 --- a/django/contrib/admin/templates/admin_doc/template_detail.html +++ b/django/contrib/admin/templates/admin_doc/template_detail.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> › <a href="../../">Documentation</a> › Templates › {{ name }}</div>{% endblock %} {% block userlinks %}<a href="../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../logout/">{% trans 'Log out' %}</a>{% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/template_filter_index.html b/django/contrib/admin/templates/admin_doc/template_filter_index.html index 30ddee9d64..72344c16cb 100644 --- a/django/contrib/admin/templates/admin_doc/template_filter_index.html +++ b/django/contrib/admin/templates/admin_doc/template_filter_index.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block coltype %}colSM{% endblock %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">Home</a> › <a href="../">Documentation</a> › filters</div>{% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/template_tag_index.html b/django/contrib/admin/templates/admin_doc/template_tag_index.html index 9051ae5dde..287475ab09 100644 --- a/django/contrib/admin/templates/admin_doc/template_tag_index.html +++ b/django/contrib/admin/templates/admin_doc/template_tag_index.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block coltype %}colSM{% endblock %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">Home</a> › <a href="../">Documentation</a> › Tags</div>{% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/view_detail.html b/django/contrib/admin/templates/admin_doc/view_detail.html index a7e920e0d6..ba90399358 100644 --- a/django/contrib/admin/templates/admin_doc/view_detail.html +++ b/django/contrib/admin/templates/admin_doc/view_detail.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> › <a href="../../">Documentation</a> › <a href="../">Views</a> › {{ name }}</div>{% endblock %} {% block userlinks %}<a href="../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../logout/">{% trans 'Log out' %}</a>{% endblock %} diff --git a/django/contrib/admin/templates/admin_doc/view_index.html b/django/contrib/admin/templates/admin_doc/view_index.html index a054ceb66b..caab8a2e71 100644 --- a/django/contrib/admin/templates/admin_doc/view_index.html +++ b/django/contrib/admin/templates/admin_doc/view_index.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %}
+{% extends "admin/base_site.html" %}
{% load i18n %}
{% block coltype %}colSM{% endblock %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">Home</a> › <a href="../">Documentation</a> › Views</div>{% endblock %}
diff --git a/django/contrib/admin/templates/registration/logged_out.html b/django/contrib/admin/templates/registration/logged_out.html index 756f82aadc..d339ef0a49 100644 --- a/django/contrib/admin/templates/registration/logged_out.html +++ b/django/contrib/admin/templates/registration/logged_out.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a></div>{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html index f163e55a68..85e1bf216c 100644 --- a/django/contrib/admin/templates/registration/password_change_done.html +++ b/django/contrib/admin/templates/registration/password_change_done.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> › {% trans 'Password change' %}</div>{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index 096e66ce13..036d56212c 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block userlinks %}<a href="../doc/">{% trans 'Documentation' %}</a> / {% trans 'Change password' %} / <a href="../logout/">{% trans 'Log out' %}</a>{% endblock %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> › {% trans 'Password change' %}</div>{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html index dff293c931..f97b5688c2 100644 --- a/django/contrib/admin/templates/registration/password_reset_done.html +++ b/django/contrib/admin/templates/registration/password_reset_done.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> › {% trans 'Password reset' %}</div>{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index 1b6a2c9a17..423821ba60 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -1,4 +1,4 @@ -{% extends "admin/base_site" %} +{% extends "admin/base_site.html" %} {% load i18n %} {% block breadcrumbs %}<div class="breadcrumbs"><a href="../">{% trans 'Home' %}</a> › {% trans 'Password reset' %}</div>{% endblock %} diff --git a/django/contrib/admin/templates/widget/foreign.html b/django/contrib/admin/templates/widget/foreign.html index 582d65df7f..6b43d044bd 100644 --- a/django/contrib/admin/templates/widget/foreign.html +++ b/django/contrib/admin/templates/widget/foreign.html @@ -1,12 +1,20 @@ {% load admin_modify adminmedia %} {% output_all bound_field.form_fields %} {% if bound_field.raw_id_admin %} -{% if bound_field.field.rel.limit_choices_to %} - <a href="../../../{{ bound_field.field.rel.to.app_label }}/{{ bound_field.field.rel.to.module_name }}/?{% for limit_choice in bound_field.field.rel.limit_choices_to.items %}{% if not forloop.first %}{{"&"|escape}}{% endif %}{{ limit_choice|join:"=" }}{% endfor %}" class="related-lookup" id="lookup_{{ bound_field.element_id }}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a> -{% else %} - <a href="../../../{{ bound_field.field.rel.to.app_label }}/{{ bound_field.field.rel.to.module_name }}/" class="related-lookup" id="lookup_{{ bound_field.element_id }}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a> -{% endif %} + {% if bound_field.field.rel.limit_choices_to %} + <a href="{{ bound_field.related_url }}?{% for limit_choice in bound_field.field.rel.limit_choices_to.items %}{% if not forloop.first %}&{% endif %}{{ limit_choice|join:"=" }}{% endfor %}" class="related-lookup" id="lookup_{{ bound_field.element_id }}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a> + {% else %} + <a href="{{ bound_field.related_url }}" class="related-lookup" id="lookup_{{ bound_field.element_id }}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a> + {% endif %} {% else %} {% if bound_field.needs_add_label %} - <a href="../../../{{ bound_field.field.rel.to.app_label }}/{{ bound_field.field.rel.to.module_name }}/add/" class="add-another" id="add_{{ bound_field.element_id }}" onclick="return showAddAnotherPopup(this);"> <img src="{% admin_media_prefix %}img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"/></a> + <a href="{{ bound_field.related_url }}add/" class="add-another" id="add_{{ bound_field.element_id }}" onclick="return showAddAnotherPopup(this);"> <img src="{% admin_media_prefix %}img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another"/></a> {% endif %}{% endif %} +{% if change %} + {% if bound_field.field.primary_key %} + {{ bound_field.original_value }} + {% endif %} + {% if bound_field.raw_id_admin %} + {% if bound_field.existing_display %} <strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %} + {% endif %} +{% endif %} diff --git a/django/contrib/admin/templates/widget/many_to_many.html b/django/contrib/admin/templates/widget/many_to_many.html index 151fe04f30..a93aa65f73 100644 --- a/django/contrib/admin/templates/widget/many_to_many.html +++ b/django/contrib/admin/templates/widget/many_to_many.html @@ -1 +1 @@ -{% include "widget/foreign" %} +{% include "widget/foreign.html" %} diff --git a/django/contrib/admin/templates/widget/one_to_one.html b/django/contrib/admin/templates/widget/one_to_one.html index 151fe04f30..a79a12314f 100644 --- a/django/contrib/admin/templates/widget/one_to_one.html +++ b/django/contrib/admin/templates/widget/one_to_one.html @@ -1 +1,2 @@ -{% include "widget/foreign" %} +{% if add %}{% include "widget/foreign.html" %}{% endif %} +{% if change %}{% if bound_field.existing_display %} <strong>{{ bound_field.existing_display|truncatewords:"14" }}</strong>{% endif %}{% endif %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index b87fd15e60..5a6b6a24e9 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -1,14 +1,15 @@ -from django.contrib.admin.views.main import MAX_SHOW_ALL_ALLOWED, DEFAULT_RESULTS_PER_PAGE, ALL_VAR +from django import template +from django.conf import settings +from django.contrib.admin.views.main import MAX_SHOW_ALL_ALLOWED, ALL_VAR from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR from django.contrib.admin.views.main import IS_POPUP_VAR, EMPTY_CHANGELIST_VALUE, MONTHS -from django.core import meta, template from django.core.exceptions import ObjectDoesNotExist +from django.db import models from django.utils import dateformat from django.utils.html import escape from django.utils.text import capfirst from django.utils.translation import get_date_formats -from django.conf.settings import ADMIN_MEDIA_PREFIX -from django.core.template import Library +from django.template import Library register = Library() @@ -16,11 +17,11 @@ DOT = '.' def paginator_number(cl,i): if i == DOT: - return '... ' + return '... ' elif i == cl.page_num: - return '<span class="this-page">%d</span> ' % (i+1) + return '<span class="this-page">%d</span> ' % (i+1) else: - return '<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1) + return '<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1) paginator_number = register.simple_tag(paginator_number) def pagination(cl): @@ -64,7 +65,7 @@ def pagination(cl): 'ALL_VAR': ALL_VAR, '1': 1, } -pagination = register.inclusion_tag('admin/pagination')(pagination) +pagination = register.inclusion_tag('admin/pagination.html')(pagination) def result_headers(cl): lookup_opts = cl.lookup_opts @@ -72,22 +73,22 @@ def result_headers(cl): for i, field_name in enumerate(lookup_opts.admin.list_display): try: f = lookup_opts.get_field(field_name) - except meta.FieldDoesNotExist: + except models.FieldDoesNotExist: # For non-field list_display values, check for the function # attribute "short_description". If that doesn't exist, fall - # back to the method name. And __repr__ is a special-case. - if field_name == '__repr__': + # back to the method name. And __str__ is a special-case. + if field_name == '__str__': header = lookup_opts.verbose_name else: - func = getattr(cl.mod.Klass, field_name) # Let AttributeErrors propagate. + attr = getattr(cl.model, field_name) # Let AttributeErrors propagate. try: - header = func.short_description + header = attr.short_description except AttributeError: - header = func.__name__.replace('_', ' ') + header = field_name.replace('_', ' ') # Non-field list_display values don't get ordering capability. yield {"text": header} else: - if isinstance(f.rel, meta.ManyToOneRel) and f.null: + if isinstance(f.rel, models.ManyToOneRel) and f.null: yield {"text": f.verbose_name} else: th_classes = [] @@ -108,34 +109,37 @@ def items_for_result(cl, result): row_class = '' try: f = cl.lookup_opts.get_field(field_name) - except meta.FieldDoesNotExist: - # For non-field list_display values, the value is a method - # name. Execute the method. + except models.FieldDoesNotExist: + # For non-field list_display values, the value is either a method + # or a property. try: - func = getattr(result, field_name) - result_repr = str(func()) + attr = getattr(result, field_name) + allow_tags = getattr(attr, 'allow_tags', False) + if callable(attr): + attr = attr() + result_repr = str(attr) except AttributeError, ObjectDoesNotExist: result_repr = EMPTY_CHANGELIST_VALUE else: # Strip HTML tags in the resulting text, except if the # function has an "allow_tags" attribute set to True. - if not getattr(func, 'allow_tags', False): + if not allow_tags: result_repr = escape(result_repr) else: field_val = getattr(result, f.attname) - if isinstance(f.rel, meta.ManyToOneRel): + if isinstance(f.rel, models.ManyToOneRel): if field_val is not None: - result_repr = getattr(result, 'get_%s' % f.name)() + result_repr = getattr(result, f.name) else: result_repr = EMPTY_CHANGELIST_VALUE # Dates and times are special: They're formatted in a certain way. - elif isinstance(f, meta.DateField) or isinstance(f, meta.TimeField): + elif isinstance(f, models.DateField) or isinstance(f, models.TimeField): if field_val: (date_format, datetime_format, time_format) = get_date_formats() - if isinstance(f, meta.DateTimeField): + if isinstance(f, models.DateTimeField): result_repr = capfirst(dateformat.format(field_val, datetime_format)) - elif isinstance(f, meta.TimeField): + elif isinstance(f, models.TimeField): result_repr = capfirst(dateformat.time_format(field_val, time_format)) else: result_repr = capfirst(dateformat.format(field_val, date_format)) @@ -143,15 +147,15 @@ def items_for_result(cl, result): result_repr = EMPTY_CHANGELIST_VALUE row_class = ' class="nowrap"' # Booleans are special: We use images. - elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField): + elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField): BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} - result_repr = '<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val) + result_repr = '<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val) # ImageFields are special: Use a thumbnail. - elif isinstance(f, meta.ImageField): + elif isinstance(f, models.ImageField): from django.parts.media.photos import get_thumbnail_url result_repr = '<img src="%s" alt="%s" title="%s" />' % (get_thumbnail_url(getattr(result, 'get_%s_url' % f.name)(), '120'), field_val, field_val) # FloatFields are special: Zero-pad the decimals. - elif isinstance(f, meta.FloatField): + elif isinstance(f, models.FloatField): if field_val is not None: result_repr = ('%%.%sf' % f.decimal_places) % field_val else: @@ -163,7 +167,7 @@ def items_for_result(cl, result): else: result_repr = escape(str(field_val)) if result_repr == '': - result_repr = ' ' + result_repr = ' ' if first: # First column is a special case first = False url = cl.url_for_result(result) @@ -181,28 +185,20 @@ def result_list(cl): return {'cl': cl, 'result_headers': list(result_headers(cl)), 'results': list(results(cl))} -result_list = register.inclusion_tag("admin/change_list_results")(result_list) +result_list = register.inclusion_tag("admin/change_list_results.html")(result_list) def date_hierarchy(cl): - lookup_opts, params, lookup_params, lookup_mod = \ - cl.lookup_opts, cl.params, cl.lookup_params, cl.lookup_mod - - if lookup_opts.admin.date_hierarchy: - field_name = lookup_opts.admin.date_hierarchy - + if cl.lookup_opts.admin.date_hierarchy: + field_name = cl.lookup_opts.admin.date_hierarchy year_field = '%s__year' % field_name month_field = '%s__month' % field_name day_field = '%s__day' % field_name field_generic = '%s__' % field_name - year_lookup = params.get(year_field) - month_lookup = params.get(month_field) - day_lookup = params.get(day_field) - - def link(d): - return cl.get_query_string(d, [field_generic]) + year_lookup = cl.params.get(year_field) + month_lookup = cl.params.get(month_field) + day_lookup = cl.params.get(day_field) - def get_dates(unit, params): - return getattr(lookup_mod, 'get_%s_list' % field_name)(unit, **params) + link = lambda d: cl.get_query_string(d, [field_generic]) if year_lookup and month_lookup and day_lookup: month_name = MONTHS[int(month_lookup)] @@ -215,9 +211,7 @@ def date_hierarchy(cl): 'choices': [{'title': "%s %s" % (month_name, day_lookup)}] } elif year_lookup and month_lookup: - date_lookup_params = lookup_params.copy() - date_lookup_params.update({year_field: year_lookup, month_field: month_lookup}) - days = get_dates('day', date_lookup_params) + days = cl.query_set.filter(**{year_field: year_lookup, month_field: month_lookup}).dates(field_name, 'day') return { 'show': True, 'back': { @@ -230,9 +224,7 @@ def date_hierarchy(cl): } for day in days] } elif year_lookup: - date_lookup_params = lookup_params.copy() - date_lookup_params.update({year_field: year_lookup}) - months = get_dates('month', date_lookup_params) + months = cl.query_set.filter(**{year_field: year_lookup}).dates(field_name, 'month') return { 'show' : True, 'back': { @@ -240,20 +232,20 @@ def date_hierarchy(cl): 'title': _('All dates') }, 'choices': [{ - 'link': link( {year_field: year_lookup, month_field: month.month}), - 'title': "%s %s" % (month.strftime('%B') , month.year) + 'link': link({year_field: year_lookup, month_field: month.month}), + 'title': "%s %s" % (month.strftime('%B'), month.year) } for month in months] } else: - years = get_dates('year', lookup_params) + years = cl.query_set.dates(field_name, 'year') return { 'show': True, 'choices': [{ 'link': link({year_field: year.year}), 'title': year.year - } for year in years ] + } for year in years] } -date_hierarchy = register.inclusion_tag('admin/date_hierarchy')(date_hierarchy) +date_hierarchy = register.inclusion_tag('admin/date_hierarchy.html')(date_hierarchy) def search_form(cl): return { @@ -261,12 +253,12 @@ def search_form(cl): 'show_result_count': cl.result_count != cl.full_result_count and not cl.opts.one_to_one_field, 'search_var': SEARCH_VAR } -search_form = register.inclusion_tag('admin/search_form')(search_form) +search_form = register.inclusion_tag('admin/search_form.html')(search_form) def filter(cl, spec): return {'title': spec.title(), 'choices' : list(spec.choices(cl))} -filter = register.inclusion_tag('admin/filter')(filter) +filter = register.inclusion_tag('admin/filter.html')(filter) def filters(cl): return {'cl': cl} -filters = register.inclusion_tag('admin/filters')(filters) +filters = register.inclusion_tag('admin/filters.html')(filters) diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 6135e74eb4..9f646214ce 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -1,11 +1,13 @@ -from django.core import template, template_loader, meta +from django import template +from django.contrib.admin.views.main import AdminBoundField +from django.template import loader from django.utils.html import escape from django.utils.text import capfirst from django.utils.functional import curry -from django.contrib.admin.views.main import AdminBoundField -from django.core.meta.fields import BoundField, Field -from django.core.meta import BoundRelatedObject, TABULAR, STACKED -from django.conf.settings import ADMIN_MEDIA_PREFIX +from django.db import models +from django.db.models.fields import Field +from django.db.models.related import BoundRelatedObject +from django.conf import settings import re register = template.Library() @@ -16,30 +18,28 @@ def class_name_to_underscored(name): return '_'.join([s.lower() for s in word_re.findall(name)[:-1]]) def include_admin_script(script_path): - return '<script type="text/javascript" src="%s%s"></script>' % (ADMIN_MEDIA_PREFIX, script_path) + return '<script type="text/javascript" src="%s%s"></script>' % (settings.ADMIN_MEDIA_PREFIX, script_path) include_admin_script = register.simple_tag(include_admin_script) -def submit_row(context, bound_manipulator): +def submit_row(context): + opts = context['opts'] change = context['change'] - add = context['add'] - show_delete = context['show_delete'] - has_delete_permission = context['has_delete_permission'] is_popup = context['is_popup'] return { - 'onclick_attrib': (bound_manipulator.ordered_objects and change + 'onclick_attrib': (opts.get_ordered_objects() and change and 'onclick="submitOrderForm();"' or ''), - 'show_delete_link': (not is_popup and has_delete_permission - and (change or show_delete)), - 'show_save_as_new': not is_popup and change and bound_manipulator.save_as, - 'show_save_and_add_another': not is_popup and (not bound_manipulator.save_as or add), - 'show_save_and_continue': not is_popup, + 'show_delete_link': (not is_popup and context['has_delete_permission'] + and (change or context['show_delete'])), + 'show_save_as_new': not is_popup and change and opts.admin.save_as, + 'show_save_and_add_another': not is_popup and (not opts.admin.save_as or context['add']), + 'show_save_and_continue': not is_popup and context['has_change_permission'], 'show_save': True } -submit_row = register.inclusion_tag('admin/submit_line', takes_context=True)(submit_row) +submit_row = register.inclusion_tag('admin/submit_line.html', takes_context=True)(submit_row) def field_label(bound_field): class_names = [] - if isinstance(bound_field.field, meta.BooleanField): + if isinstance(bound_field.field, models.BooleanField): class_names.append("vCheckboxLabel") colon = "" else: @@ -64,16 +64,15 @@ class FieldWidgetNode(template.Node): if not cls.nodelists.has_key(klass): try: field_class_name = klass.__name__ - template_name = "widget/%s" % \ - class_name_to_underscored(field_class_name) - nodelist = template_loader.get_template(template_name).nodelist + template_name = "widget/%s.html" % class_name_to_underscored(field_class_name) + nodelist = loader.get_template(template_name).nodelist except template.TemplateDoesNotExist: super_klass = bool(klass.__bases__) and klass.__bases__[0] or None if super_klass and super_klass != Field: nodelist = cls.get_nodelist(super_klass) else: if not cls.default: - cls.default = template_loader.get_template("widget/default").nodelist + cls.default = loader.get_template("widget/default.html").nodelist nodelist = cls.default cls.nodelists[klass] = nodelist @@ -97,21 +96,22 @@ class FieldWrapper(object): self.field = field def needs_header(self): - return not isinstance(self.field, meta.AutoField) + return not isinstance(self.field, models.AutoField) def header_class_attribute(self): return self.field.blank and ' class="optional"' or '' def use_raw_id_admin(self): - return isinstance(self.field.rel, (meta.ManyToOneRel, meta.ManyToManyRel)) \ + return isinstance(self.field.rel, (models.ManyToOneRel, models.ManyToManyRel)) \ and self.field.rel.raw_id_admin class FormFieldCollectionWrapper(object): - def __init__(self, field_mapping, fields): + def __init__(self, field_mapping, fields, index): self.field_mapping = field_mapping self.fields = fields self.bound_fields = [AdminBoundField(field, self.field_mapping, field_mapping['original']) for field in self.fields] + self.index = index class TabularBoundRelatedObject(BoundRelatedObject): def __init__(self, related_object, field_mapping, original): @@ -120,29 +120,25 @@ class TabularBoundRelatedObject(BoundRelatedObject): fields = self.relation.editable_fields() - self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping, fields) - for field_mapping in self.field_mappings] + self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping, fields, i) + for (i,field_mapping) in self.field_mappings.items() ] self.original_row_needed = max([fw.use_raw_id_admin() for fw in self.field_wrapper_list]) self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url') def template_name(self): - return "admin/edit_inline_tabular" + return "admin/edit_inline_tabular.html" class StackedBoundRelatedObject(BoundRelatedObject): def __init__(self, related_object, field_mapping, original): super(StackedBoundRelatedObject, self).__init__(related_object, field_mapping, original) fields = self.relation.editable_fields() - self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping ,fields) - for field_mapping in self.field_mappings] + self.field_mappings.fill() + self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping ,fields, i) + for (i,field_mapping) in self.field_mappings.items()] self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url') def template_name(self): - return "admin/edit_inline_stacked" - -bound_related_object_overrides = { - TABULAR: TabularBoundRelatedObject, - STACKED: StackedBoundRelatedObject, -} + return "admin/edit_inline_stacked.html" class EditInlineNode(template.Node): def __init__(self, rel_var): @@ -150,21 +146,16 @@ class EditInlineNode(template.Node): def render(self, context): relation = template.resolve_variable(self.rel_var, context) - context.push() - - klass = relation.field.rel.edit_inline - bound_related_object_class = bound_related_object_overrides.get(klass, klass) - + if relation.field.rel.edit_inline == models.TABULAR: + bound_related_object_class = TabularBoundRelatedObject + else: + bound_related_object_class = StackedBoundRelatedObject original = context.get('original', None) - bound_related_object = relation.bind(context['form'], original, bound_related_object_class) context['bound_related_object'] = bound_related_object - - t = template_loader.get_template(bound_related_object.template_name()) - + t = loader.get_template(bound_related_object.template_name()) output = t.render(context) - context.pop() return output @@ -191,30 +182,30 @@ auto_populated_field_script = register.simple_tag(auto_populated_field_script) def filter_interface_script_maybe(bound_field): f = bound_field.field - if f.rel and isinstance(f.rel, meta.ManyToManyRel) and f.rel.filter_interface: - return '<script type="text/javascript">addEvent(window, "load", function(e) {' \ + if f.rel and isinstance(f.rel, models.ManyToManyRel) and f.rel.filter_interface: + return '<script type="text/javascript">addEvent(window, "load", function(e) {' \ ' SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % ( - f.name, f.verbose_name, f.rel.filter_interface-1, ADMIN_MEDIA_PREFIX) + f.name, f.verbose_name, f.rel.filter_interface-1, settings.ADMIN_MEDIA_PREFIX) else: return '' filter_interface_script_maybe = register.simple_tag(filter_interface_script_maybe) -def do_one_arg_tag(node_factory, parser,token): - tokens = token.contents.split() - if len(tokens) != 2: - raise template.TemplateSyntaxError("%s takes 1 argument" % tokens[0]) - return node_factory(tokens[1]) - -def register_one_arg_tag(node): - tag_name = class_name_to_underscored(node.__name__) - parse_func = curry(do_one_arg_tag, node) - register.tag(tag_name, parse_func) +def field_widget(parser, token): + bits = token.contents.split() + if len(bits) != 2: + raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0] + return FieldWidgetNode(bits[1]) +field_widget = register.tag(field_widget) -register_one_arg_tag(FieldWidgetNode) -register_one_arg_tag(EditInlineNode) +def edit_inline(parser, token): + bits = token.contents.split() + if len(bits) != 2: + raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0] + return EditInlineNode(bits[1]) +edit_inline = register.tag(edit_inline) def admin_field_line(context, argument_val): - if (isinstance(argument_val, BoundField)): + if isinstance(argument_val, AdminBoundField): bound_fields = [argument_val] else: bound_fields = [bf for bf in argument_val] @@ -229,7 +220,7 @@ def admin_field_line(context, argument_val): break # Assumes BooleanFields won't be stacked next to each other! - if isinstance(bound_fields[0].field, meta.BooleanField): + if isinstance(bound_fields[0].field, models.BooleanField): class_names.append('checkbox-row') return { @@ -238,8 +229,4 @@ def admin_field_line(context, argument_val): 'bound_fields': bound_fields, 'class_names': " ".join(class_names), } -admin_field_line = register.inclusion_tag('admin/field_line', takes_context=True)(admin_field_line) - -def object_pk(bound_manip, ordered_obj): - return bound_manip.get_ordered_object_pk(ordered_obj) -object_pk = register.simple_tag(object_pk) +admin_field_line = register.inclusion_tag('admin/field_line.html', takes_context=True)(admin_field_line) diff --git a/django/contrib/admin/templatetags/adminapplist.py b/django/contrib/admin/templatetags/adminapplist.py index 7a91516ebc..c328ddf203 100644 --- a/django/contrib/admin/templatetags/adminapplist.py +++ b/django/contrib/admin/templatetags/adminapplist.py @@ -1,4 +1,5 @@ -from django.core import template +from django import template +from django.db.models import get_models register = template.Library() @@ -7,20 +8,24 @@ class AdminApplistNode(template.Node): self.varname = varname def render(self, context): - from django.core import meta + from django.db import models from django.utils.text import capfirst app_list = [] user = context['user'] - for app in meta.get_installed_model_modules(): - app_label = app.__name__[app.__name__.rindex('.')+1:] + for app in models.get_apps(): + # Determine the app_label. + app_models = get_models(app) + if not app_models: + continue + app_label = app_models[0]._meta.app_label + has_module_perms = user.has_module_perms(app_label) if has_module_perms: model_list = [] - for m in app._MODELS: + for m in app_models: if m._meta.admin: - module_name = m._meta.module_name perms = { 'add': user.has_perm("%s.%s" % (app_label, m._meta.get_add_permission())), 'change': user.has_perm("%s.%s" % (app_label, m._meta.get_change_permission())), @@ -32,7 +37,7 @@ class AdminApplistNode(template.Node): if True in perms.values(): model_list.append({ 'name': capfirst(m._meta.verbose_name_plural), - 'admin_url': '%s/%s/' % (app_label, m._meta.module_name), + 'admin_url': '%s/%s/' % (app_label, m.__name__.lower()), 'perms': perms, }) diff --git a/django/contrib/admin/templatetags/adminmedia.py b/django/contrib/admin/templatetags/adminmedia.py index cd513f6c81..266c017e3d 100644 --- a/django/contrib/admin/templatetags/adminmedia.py +++ b/django/contrib/admin/templatetags/adminmedia.py @@ -1,10 +1,11 @@ -from django.core.template import Library +from django.template import Library + register = Library() def admin_media_prefix(): try: - from django.conf.settings import ADMIN_MEDIA_PREFIX + from django.conf import settings except ImportError: return '' - return ADMIN_MEDIA_PREFIX + return settings.ADMIN_MEDIA_PREFIX admin_media_prefix = register.simple_tag(admin_media_prefix) diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index 013e07c80f..5caba2b795 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -1,5 +1,5 @@ -from django.models.admin import log -from django.core import template +from django import template +from django.contrib.admin.models import LogEntry register = template.Library() @@ -13,7 +13,7 @@ class AdminLogNode(template.Node): def render(self, context): if self.user is not None and not self.user.isdigit(): self.user = context[self.user].id - context[self.varname] = log.get_list(user__id__exact=self.user, limit=self.limit, select_related=True) + context[self.varname] = LogEntry.objects.filter(user__id__exact=self.user).select_related()[:self.limit] return '' class DoGetAdminLog: diff --git a/django/contrib/admin/urls.py b/django/contrib/admin/urls.py new file mode 100644 index 0000000000..dde848d766 --- /dev/null +++ b/django/contrib/admin/urls.py @@ -0,0 +1,31 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + ('^$', 'django.contrib.admin.views.main.index'), + ('^r/(\d+)/(\d+)/$', 'django.views.defaults.shortcut'), + ('^jsi18n/$', 'django.views.i18n.javascript_catalog', {'packages': 'django.conf'}), + ('^logout/$', 'django.contrib.auth.views.logout'), + ('^password_change/$', 'django.contrib.auth.views.password_change'), + ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'), + ('^template_validator/$', 'django.contrib.admin.views.template.template_validator'), + + # Documentation + ('^doc/$', 'django.contrib.admin.views.doc.doc_index'), + ('^doc/bookmarklets/$', 'django.contrib.admin.views.doc.bookmarklets'), + ('^doc/tags/$', 'django.contrib.admin.views.doc.template_tag_index'), + ('^doc/filters/$', 'django.contrib.admin.views.doc.template_filter_index'), + ('^doc/views/$', 'django.contrib.admin.views.doc.view_index'), + ('^doc/views/jump/$', 'django.contrib.admin.views.doc.jump_to_view'), + ('^doc/views/(?P<view>[^/]+)/$', 'django.contrib.admin.views.doc.view_detail'), + ('^doc/models/$', 'django.contrib.admin.views.doc.model_index'), + ('^doc/models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$', 'django.contrib.admin.views.doc.model_detail'), +# ('^doc/templates/$', 'django.views.admin.doc.template_index'), + ('^doc/templates/(?P<template>.*)/$', 'django.contrib.admin.views.doc.template_detail'), + + # Add/change/delete/history + ('^([^/]+)/([^/]+)/$', 'django.contrib.admin.views.main.change_list'), + ('^([^/]+)/([^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'), + ('^([^/]+)/([^/]+)/(.+)/history/$', 'django.contrib.admin.views.main.history'), + ('^([^/]+)/([^/]+)/(.+)/delete/$', 'django.contrib.admin.views.main.delete_stage'), + ('^([^/]+)/([^/]+)/(.+)/$', 'django.contrib.admin.views.main.change_stage'), +) diff --git a/django/contrib/admin/urls/admin.py b/django/contrib/admin/urls/admin.py deleted file mode 100644 index 3f4dbab419..0000000000 --- a/django/contrib/admin/urls/admin.py +++ /dev/null @@ -1,58 +0,0 @@ -from django.conf.urls.defaults import * -from django.conf.settings import INSTALLED_APPS - -urlpatterns = ( - ('^$', 'django.contrib.admin.views.main.index'), - ('^jsi18n/$', 'django.views.i18n.javascript_catalog', {'packages': 'django.conf'}), - ('^logout/$', 'django.views.auth.login.logout'), - ('^password_change/$', 'django.views.registration.passwords.password_change'), - ('^password_change/done/$', 'django.views.registration.passwords.password_change_done'), - ('^template_validator/$', 'django.contrib.admin.views.template.template_validator'), - - # Documentation - ('^doc/$', 'django.contrib.admin.views.doc.doc_index'), - ('^doc/bookmarklets/$', 'django.contrib.admin.views.doc.bookmarklets'), - ('^doc/tags/$', 'django.contrib.admin.views.doc.template_tag_index'), - ('^doc/filters/$', 'django.contrib.admin.views.doc.template_filter_index'), - ('^doc/views/$', 'django.contrib.admin.views.doc.view_index'), - ('^doc/views/jump/$', 'django.contrib.admin.views.doc.jump_to_view'), - ('^doc/views/(?P<view>[^/]+)/$', 'django.contrib.admin.views.doc.view_detail'), - ('^doc/models/$', 'django.contrib.admin.views.doc.model_index'), - ('^doc/models/(?P<model>[^/]+)/$', 'django.contrib.admin.views.doc.model_detail'), -# ('^doc/templates/$', 'django.views.admin.doc.template_index'), - ('^doc/templates/(?P<template>.*)/$', 'django.contrib.admin.views.doc.template_detail'), -) - -if 'ellington.events' in INSTALLED_APPS: - urlpatterns += ( - ("^events/usersubmittedevents/(?P<object_id>\d+)/$", 'ellington.events.views.admin.user_submitted_event_change_stage'), - ("^events/usersubmittedevents/(?P<object_id>\d+)/delete/$", 'ellington.events.views.admin.user_submitted_event_delete_stage'), - ) - -if 'ellington.news' in INSTALLED_APPS: - urlpatterns += ( - ("^stories/preview/$", 'ellington.news.views.admin.story_preview'), - ("^stories/js/inlinecontrols/$", 'ellington.news.views.admin.inlinecontrols_js'), - ("^stories/js/inlinecontrols/(?P<label>[-\w]+)/$", 'ellington.news.views.admin.inlinecontrols_js_specific'), - ) - -if 'ellington.alerts' in INSTALLED_APPS: - urlpatterns += ( - ("^alerts/send/$", 'ellington.alerts.views.admin.send_alert_form'), - ("^alerts/send/do/$", 'ellington.alerts.views.admin.send_alert_action'), - ) - -if 'ellington.media' in INSTALLED_APPS: - urlpatterns += ( - ('^media/photos/caption/(?P<photo_id>\d+)/$', 'ellington.media.views.admin.get_exif_caption'), - ) - -urlpatterns += ( - # Metasystem admin pages - ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/$', 'django.contrib.admin.views.main.change_list'), - ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/add/$', 'django.contrib.admin.views.main.add_stage'), - ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/(?P<object_id>.+)/history/$', 'django.contrib.admin.views.main.history'), - ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/(?P<object_id>.+)/delete/$', 'django.contrib.admin.views.main.delete_stage'), - ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/(?P<object_id>.+)/$', 'django.contrib.admin.views.main.change_stage'), -) -urlpatterns = patterns('', *urlpatterns) diff --git a/django/contrib/admin/utils.py b/django/contrib/admin/utils.py index 80a8f2a773..c6c794c75e 100644 --- a/django/contrib/admin/utils.py +++ b/django/contrib/admin/utils.py @@ -82,18 +82,18 @@ ROLES = { def create_reference_role(rolename, urlbase): def _role(name, rawtext, text, lineno, inliner, options={}, content=[]): - node = docutils.nodes.reference(rawtext, text, refuri=(urlbase % (inliner.document.settings.link_base, text)), **options) + node = docutils.nodes.reference(rawtext, text, refuri=(urlbase % (inliner.document.settings.link_base, text.lower())), **options) return [node], [] docutils.parsers.rst.roles.register_canonical_role(rolename, _role) def default_reference_role(name, rawtext, text, lineno, inliner, options={}, content=[]): context = inliner.document.settings.default_reference_context - node = docutils.nodes.reference(rawtext, text, refuri=(ROLES[context] % (inliner.document.settings.link_base, text)), **options) + node = docutils.nodes.reference(rawtext, text, refuri=(ROLES[context] % (inliner.document.settings.link_base, text.lower())), **options) return [node], [] if docutils_is_available: docutils.parsers.rst.roles.register_canonical_role('cmsreference', default_reference_role) docutils.parsers.rst.roles.DEFAULT_INTERPRETED_ROLE = 'cmsreference' - for (name, urlbase) in ROLES.items(): + for name, urlbase in ROLES.items(): create_reference_role(name, urlbase) diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index 5ddc17fa85..d984077dfb 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -1,7 +1,7 @@ -from django.core.extensions import DjangoContext, render_to_response -from django.conf.settings import SECRET_KEY -from django.models.auth import users -from django.utils import httpwrappers +from django import http, template +from django.conf import settings +from django.contrib.auth.models import User, SESSION_KEY +from django.shortcuts import render_to_response from django.utils.translation import gettext_lazy import base64, datetime, md5 import cPickle as pickle @@ -19,22 +19,22 @@ def _display_login_form(request, error_message=''): post_data = _encode_post_data(request.POST) else: post_data = _encode_post_data({}) - return render_to_response('admin/login', { + return render_to_response('admin/login.html', { 'title': _('Log in'), 'app_path': request.path, 'post_data': post_data, 'error_message': error_message - }, context_instance=DjangoContext(request)) + }, context_instance=template.RequestContext(request)) def _encode_post_data(post_data): pickled = pickle.dumps(post_data) - pickled_md5 = md5.new(pickled + SECRET_KEY).hexdigest() + pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest() return base64.encodestring(pickled + pickled_md5) def _decode_post_data(encoded_data): encoded_data = base64.decodestring(encoded_data) pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] - if md5.new(pickled + SECRET_KEY).hexdigest() != tamper_check: + if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: from django.core.exceptions import SuspiciousOperation raise SuspiciousOperation, "User may have tampered with session cookie." return pickle.loads(pickled) @@ -53,7 +53,7 @@ def staff_member_required(view_func): request.POST = _decode_post_data(request.POST['post_data']) return view_func(request, *args, **kwargs) - assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.middleware.sessions.SessionMiddleware'." + assert hasattr(request, 'session'), "The Django admin requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." # If this isn't already the login page, display it. if not request.POST.has_key(LOGIN_FORM_KEY): @@ -71,14 +71,14 @@ def staff_member_required(view_func): # Check the password. username = request.POST.get('username', '') try: - user = users.get_object(username__exact=username, is_staff__exact=True) - except users.UserDoesNotExist: + user = User.objects.get(username=username, is_staff=True) + except User.DoesNotExist: message = ERROR_MESSAGE if '@' in username: # Mistakenly entered e-mail address instead of username? Look it up. try: - user = users.get_object(email__exact=username) - except users.UserDoesNotExist: + user = User.objects.get(email=username) + except User.DoesNotExist: message = _("Usernames cannot contain the '@' character.") else: message = _("Your e-mail address is not your username. Try '%s' instead.") % user.username @@ -87,7 +87,7 @@ def staff_member_required(view_func): # The user data is correct; log in the user in and continue. else: if user.check_password(request.POST.get('password', '')): - request.session[users.SESSION_KEY] = user.id + request.session[SESSION_KEY] = user.id user.last_login = datetime.datetime.now() user.save() if request.POST.has_key('post_data'): @@ -99,7 +99,7 @@ def staff_member_required(view_func): return view_func(request, *args, **kwargs) else: request.session.delete_test_cookie() - return httpwrappers.HttpResponseRedirect(request.path) + return http.HttpResponseRedirect(request.path) else: return _display_login_form(request, ERROR_MESSAGE) diff --git a/django/contrib/admin/views/doc.py b/django/contrib/admin/views/doc.py index 9a6d3ec400..f3675b6adf 100644 --- a/django/contrib/admin/views/doc.py +++ b/django/contrib/admin/views/doc.py @@ -1,12 +1,14 @@ -from django.core import meta -from django import templatetags +from django import template, templatetags +from django.template import RequestContext from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required -from django.models.core import sites -from django.core.extensions import DjangoContext, render_to_response -from django.core.exceptions import Http404, ViewDoesNotExist -from django.core import template, urlresolvers +from django.db import models +from django.shortcuts import render_to_response +from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist +from django.http import Http404, get_host +from django.core import urlresolvers from django.contrib.admin import utils +from django.contrib.sites.models import Site import inspect, os, re # Exclude methods starting with these strings from documentation @@ -15,15 +17,15 @@ MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_') def doc_index(request): if not utils.docutils_is_available: return missing_docutils_page(request) - return render_to_response('admin_doc/index', context_instance=DjangoContext(request)) + return render_to_response('admin_doc/index.html', context_instance=RequestContext(request)) doc_index = staff_member_required(doc_index) def bookmarklets(request): # Hack! This couples this view to the URL it lives at. admin_root = request.path[:-len('doc/bookmarklets/')] - return render_to_response('admin_doc/bookmarklets', { - 'admin_url': "%s://%s%s" % (os.environ.get('HTTPS') == 'on' and 'https' or 'http', request.META['HTTP_HOST'], admin_root), - }, context_instance=DjangoContext(request)) + return render_to_response('admin_doc/bookmarklets.html', { + 'admin_url': "%s://%s%s" % (os.environ.get('HTTPS') == 'on' and 'https' or 'http', get_host(request), admin_root), + }, context_instance=RequestContext(request)) bookmarklets = staff_member_required(bookmarklets) def template_tag_index(request): @@ -54,7 +56,7 @@ def template_tag_index(request): 'library': tag_library, }) - return render_to_response('admin_doc/template_tag_index', {'tags': tags}, context_instance=DjangoContext(request)) + return render_to_response('admin_doc/template_tag_index.html', {'tags': tags}, context_instance=RequestContext(request)) template_tag_index = staff_member_required(template_tag_index) def template_filter_index(request): @@ -84,16 +86,20 @@ def template_filter_index(request): 'meta': metadata, 'library': tag_library, }) - return render_to_response('admin_doc/template_filter_index', {'filters': filters}, context_instance=DjangoContext(request)) + return render_to_response('admin_doc/template_filter_index.html', {'filters': filters}, context_instance=RequestContext(request)) template_filter_index = staff_member_required(template_filter_index) def view_index(request): if not utils.docutils_is_available: return missing_docutils_page(request) + if settings.ADMIN_FOR: + settings_modules = [__import__(m, '', '', ['']) for m in settings.ADMIN_FOR] + else: + settings_modules = [settings] + views = [] - for site_settings_module in settings.ADMIN_FOR: - settings_mod = __import__(site_settings_module, '', '', ['']) + for settings_mod in settings_modules: urlconf = __import__(settings_mod.ROOT_URLCONF, '', '', ['']) view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns) for (func, regex) in view_functions: @@ -101,10 +107,10 @@ def view_index(request): 'name': func.__name__, 'module': func.__module__, 'site_id': settings_mod.SITE_ID, - 'site': sites.get_object(pk=settings_mod.SITE_ID), + 'site': Site.objects.get(pk=settings_mod.SITE_ID), 'url': simplify_regex(regex), }) - return render_to_response('admin_doc/view_index', {'views': views}, context_instance=DjangoContext(request)) + return render_to_response('admin_doc/view_index.html', {'views': views}, context_instance=RequestContext(request)) view_index = staff_member_required(view_index) def view_detail(request, view): @@ -123,51 +129,63 @@ def view_detail(request, view): body = utils.parse_rst(body, 'view', 'view:' + view) for key in metadata: metadata[key] = utils.parse_rst(metadata[key], 'model', 'view:' + view) - return render_to_response('admin_doc/view_detail', { + return render_to_response('admin_doc/view_detail.html', { 'name': view, 'summary': title, 'body': body, 'meta': metadata, - }, context_instance=DjangoContext(request)) + }, context_instance=RequestContext(request)) view_detail = staff_member_required(view_detail) def model_index(request): if not utils.docutils_is_available: return missing_docutils_page(request) - models = [] - for app in meta.get_installed_model_modules(): - for model in app._MODELS: - opts = model._meta - models.append({ - 'name': '%s.%s' % (opts.app_label, opts.module_name), - 'module': opts.app_label, - 'class': opts.module_name, - }) - return render_to_response('admin_doc/model_index', {'models': models}, context_instance=DjangoContext(request)) + m_list = [m._meta for m in models.get_models()] + return render_to_response('admin_doc/model_index.html', {'models': m_list}, context_instance=RequestContext(request)) model_index = staff_member_required(model_index) -def model_detail(request, model): +def model_detail(request, app_label, model_name): if not utils.docutils_is_available: return missing_docutils_page(request) + # Get the model class. try: - model = meta.get_app(model) - except ImportError: - raise Http404 - opts = model.Klass._meta + app_mod = models.get_app(app_label) + except ImproperlyConfigured: + raise Http404, "App %r not found" % app_label + model = None + for m in models.get_models(app_mod): + if m._meta.object_name.lower() == model_name: + model = m + break + if model is None: + raise Http404, "Model %r not found in app %r" % (model_name, app_label) - # Gather fields/field descriptions + opts = model._meta + + # Gather fields/field descriptions. fields = [] for field in opts.fields: + # ForeignKey is a special case since the field will actually be a + # descriptor that returns the other object + if isinstance(field, models.ForeignKey): + data_type = related_object_name = field.rel.to.__name__ + app_label = field.rel.to._meta.app_label + verbose = utils.parse_rst(("the related `%s.%s` object" % (app_label, data_type)), 'model', 'model:' + data_type) + else: + data_type = get_readable_field_data_type(field) + verbose = field.verbose_name fields.append({ 'name': field.name, - 'data_type': get_readable_field_data_type(field), - 'verbose': field.verbose_name, + 'data_type': data_type, + 'verbose': verbose, 'help': field.help_text, }) - for func_name, func in model.Klass.__dict__.items(): - if callable(func) and len(inspect.getargspec(func)[0]) == 0: + + # Gather model methods. + for func_name, func in model.__dict__.items(): + if (inspect.isfunction(func) and len(inspect.getargspec(func)[0]) == 1): try: for exclude in MODEL_METHODS_EXCLUDE: if func_name.startswith(exclude): @@ -182,12 +200,26 @@ def model_detail(request, model): 'data_type': get_return_data_type(func_name), 'verbose': verbose, }) - return render_to_response('admin_doc/model_detail', { - 'name': '%s.%s' % (opts.app_label, opts.module_name), - 'summary': "Fields on %s objects" % opts.verbose_name, + + # Gather related objects + for rel in opts.get_all_related_objects(): + verbose = "related `%s.%s` objects" % (rel.opts.app_label, rel.opts.object_name) + accessor = rel.get_accessor_name() + fields.append({ + 'name' : "%s.all" % accessor, + 'verbose' : utils.parse_rst("all " + verbose , 'model', 'model:' + opts.module_name), + }) + fields.append({ + 'name' : "%s.count" % accessor, + 'verbose' : utils.parse_rst("number of " + verbose , 'model', 'model:' + opts.module_name), + }) + + return render_to_response('admin_doc/model_detail.html', { + 'name': '%s.%s' % (opts.app_label, opts.object_name), + 'summary': "Fields on %s objects" % opts.object_name, 'description': model.__doc__, 'fields': fields, - }, context_instance=DjangoContext(request)) + }, context_instance=RequestContext(request)) model_detail = staff_member_required(model_detail) def template_detail(request, template): @@ -201,13 +233,13 @@ def template_detail(request, template): 'exists': os.path.exists(template_file), 'contents': lambda: os.path.exists(template_file) and open(template_file).read() or '', 'site_id': settings_mod.SITE_ID, - 'site': sites.get_object(pk=settings_mod.SITE_ID), + 'site': Site.objects.get(pk=settings_mod.SITE_ID), 'order': list(settings_mod.TEMPLATE_DIRS).index(dir), }) - return render_to_response('admin_doc/template_detail', { + return render_to_response('admin_doc/template_detail.html', { 'name': template, 'templates': templates, - }, context_instance=DjangoContext(request)) + }, context_instance=RequestContext(request)) template_detail = staff_member_required(template_detail) #################### @@ -216,7 +248,7 @@ template_detail = staff_member_required(template_detail) def missing_docutils_page(request): """Display an error message for people without docutils""" - return render_to_response('admin_doc/missing_docutils') + return render_to_response('admin_doc/missing_docutils.html') def load_all_installed_template_libraries(): # Load/register all template tag libraries from installed apps. @@ -271,9 +303,6 @@ DATA_TYPE_MAPPING = { } def get_readable_field_data_type(field): - # ForeignKey is a special case. Use the field type of the relation. - if field.get_internal_type() == 'ForeignKey': - field = field.rel.get_related_field() return DATA_TYPE_MAPPING[field.get_internal_type()] % field.__dict__ def extract_views_from_urlpatterns(urlpatterns, base=''): @@ -295,15 +324,23 @@ def extract_views_from_urlpatterns(urlpatterns, base=''): raise TypeError, "%s does not appear to be a urlpattern object" % p return views -# Clean up urlpattern regexes into something somewhat readable by Mere Humans: -# turns something like "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$" -# into "<sport_slug>/athletes/<athlete_slug>/" - named_group_matcher = re.compile(r'\(\?P(<\w+>).+?\)') +non_named_group_matcher = re.compile(r'\(.*?\)') def simplify_regex(pattern): + """ + Clean up urlpattern regexes into something somewhat readable by Mere Humans: + turns something like "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$" + into "<sport_slug>/athletes/<athlete_slug>/" + """ + # handle named groups first pattern = named_group_matcher.sub(lambda m: m.group(1), pattern) - pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/') + + # handle non-named groups + pattern = non_named_group_matcher.sub("<var>", pattern) + + # clean up any outstanding regex-y characters. + pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/').replace('\\', '') if not pattern.startswith('/'): pattern = '/' + pattern return pattern diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 3d92a7d949..9913914858 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,32 +1,34 @@ -# Generic admin views. -from django.contrib.admin.views.decorators import staff_member_required +from django import forms, template +from django.conf import settings from django.contrib.admin.filterspecs import FilterSpec -from django.core import formfields, meta, template -from django.core.template import loader -from django.core.meta.fields import BoundField, BoundFieldLine, BoundFieldSet -from django.core.exceptions import Http404, ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied -from django.core.extensions import DjangoContext as Context -from django.core.extensions import get_object_or_404, render_to_response +from django.contrib.admin.views.decorators import staff_member_required +from django.views.decorators.cache import never_cache +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied from django.core.paginator import ObjectPaginator, InvalidPage -from django.conf.settings import ADMIN_MEDIA_PREFIX -try: - from django.models.admin import log -except ImportError: - raise ImproperlyConfigured, "You don't have 'django.contrib.admin' in INSTALLED_APPS." -from django.utils.html import escape -from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect -from django.utils.text import capfirst, get_text_list +from django.shortcuts import get_object_or_404, render_to_response +from django.db import models +from django.db.models.query import handle_legacy_orderlist, QuerySet +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.template import loader from django.utils import dateformat from django.utils.dates import MONTHS from django.utils.html import escape +from django.utils.text import capfirst, get_text_list import operator -# The system will display a "Show all" link only if the total result count -# is less than or equal to this setting. -MAX_SHOW_ALL_ALLOWED = 200 +from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION +if not LogEntry._meta.installed: + raise ImproperlyConfigured, "You'll need to put 'django.contrib.admin' in your INSTALLED_APPS setting before you can use the admin application." -DEFAULT_RESULTS_PER_PAGE = 100 +if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: + raise ImproperlyConfigured, "You'll need to put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting before you can use the admin application." + +# The system will display a "Show all" link on the change list only if the +# total result count is less than or equal to this setting. +MAX_SHOW_ALL_ALLOWED = 200 +# Changelist settings ALL_VAR = 'all' ORDER_VAR = 'o' ORDER_TYPE_VAR = 'ot' @@ -34,229 +36,59 @@ PAGE_VAR = 'p' SEARCH_VAR = 'q' IS_POPUP_VAR = 'pop' -# Text to display within changelist table cells if the value is blank. +# Text to display within change-list table cells if the value is blank. EMPTY_CHANGELIST_VALUE = '(None)' -def _get_mod_opts(app_label, module_name): - "Helper function that returns a tuple of (module, opts), raising Http404 if necessary." - try: - mod = meta.get_module(app_label, module_name) - except ImportError: - raise Http404 # Invalid app or module name. Maybe it's not in INSTALLED_APPS. - opts = mod.Klass._meta - if not opts.admin: - raise Http404 # This object is valid but has no admin interface. - return mod, opts - -def index(request): - return render_to_response('admin/index', {'title': _('Site administration')}, context_instance=Context(request)) -index = staff_member_required(index) +use_raw_id_admin = lambda field: isinstance(field.rel, (models.ManyToOneRel, models.ManyToManyRel)) and field.rel.raw_id_admin class IncorrectLookupParameters(Exception): pass -class ChangeList(object): - def __init__(self, request, app_label, module_name): - self.get_modules_and_options(app_label, module_name, request) - self.get_search_parameters(request) - self.get_ordering() - self.query = request.GET.get(SEARCH_VAR, '') - self.get_lookup_params() - self.get_results(request) - self.title = (self.is_popup - and _('Select %s') % self.opts.verbose_name - or _('Select %s to change') % self.opts.verbose_name) - self.get_filters(request) - self.pk_attname = self.lookup_opts.pk.attname - - def get_filters(self, request): - self.filter_specs = [] - if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field: - filter_fields = [self.lookup_opts.get_field(field_name) \ - for field_name in self.lookup_opts.admin.list_filter] - for f in filter_fields: - spec = FilterSpec.create(f, request, self.params) - if spec and spec.has_output(): - self.filter_specs.append(spec) - self.has_filters = bool(self.filter_specs) +def quote(s): + """ + Ensure that primary key values do not confuse the admin URLs by escaping + any '/', '_' and ':' characters. Similar to urllib.quote, except that the + quoting is slightly different so that it doesn't get autoamtically + unquoted by the web browser. + """ + if type(s) != type(''): + return s + res = list(s) + for i in range(len(res)): + c = res[i] + if c in ':/_': + res[i] = '_%02X' % ord(c) + return ''.join(res) - def get_query_string(self, new_params={}, remove=[]): - p = self.params.copy() - for r in remove: - for k in p.keys(): - if k.startswith(r): - del p[k] - for k, v in new_params.items(): - if p.has_key(k) and v is None: - del p[k] - elif v is not None: - p[k] = v - return '?' + '&'.join(['%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20') - - def get_modules_and_options(self, app_label, module_name, request): - self.mod, self.opts = _get_mod_opts(app_label, module_name) - if not request.user.has_perm(app_label + '.' + self.opts.get_change_permission()): - raise PermissionDenied - - self.lookup_mod, self.lookup_opts = self.mod, self.opts - - def get_search_parameters(self, request): - # Get search parameters from the query string. - try: - self.page_num = int(request.GET.get(PAGE_VAR, 0)) - except ValueError: - self.page_num = 0 - self.show_all = request.GET.has_key(ALL_VAR) - self.is_popup = request.GET.has_key(IS_POPUP_VAR) - self.params = dict(request.GET.items()) - if self.params.has_key(PAGE_VAR): - del self.params[PAGE_VAR] - - def get_results(self, request): - lookup_mod, lookup_params, show_all, page_num = \ - self.lookup_mod, self.lookup_params, self.show_all, self.page_num - # Get the results. - try: - paginator = ObjectPaginator(lookup_mod, lookup_params, DEFAULT_RESULTS_PER_PAGE) - # Naked except! Because we don't have any other way of validating "params". - # They might be invalid if the keyword arguments are incorrect, or if the - # values are not in the correct type (which would result in a database - # error). - except: - raise IncorrectLookupParameters() - - # Get the total number of objects, with no filters applied. - real_lookup_params = lookup_params.copy() - del real_lookup_params['order_by'] - if real_lookup_params: - full_result_count = lookup_mod.get_count() - else: - full_result_count = paginator.hits - del real_lookup_params - result_count = paginator.hits - can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED - multi_page = result_count > DEFAULT_RESULTS_PER_PAGE - - # Get the list of objects to display on this page. - if (show_all and can_show_all) or not multi_page: - result_list = lookup_mod.get_list(**lookup_params) - else: +def unquote(s): + """ + Undo the effects of quote(). Based heavily on urllib.unquote(). + """ + mychr = chr + myatoi = int + list = s.split('_') + res = [list[0]] + myappend = res.append + del list[0] + for item in list: + if item[1:2]: try: - result_list = paginator.get_page(page_num) - except InvalidPage: - result_list = [] - (self.result_count, self.full_result_count, self.result_list, - self.can_show_all, self.multi_page, self.paginator) = (result_count, - full_result_count, result_list, can_show_all, multi_page, paginator ) - - def url_for_result(self, result): - return "%s/" % getattr(result, self.pk_attname) - - def get_ordering(self): - lookup_opts, params = self.lookup_opts, self.params - # For ordering, first check the "ordering" parameter in the admin options, - # then check the object's default ordering. If neither of those exist, - # order descending by ID by default. Finally, look for manually-specified - # ordering from the query string. - ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] - - # Normalize it to new-style ordering. - ordering = meta.handle_legacy_orderlist(ordering) - - if ordering[0].startswith('-'): - order_field, order_type = ordering[0][1:], 'desc' - else: - order_field, order_type = ordering[0], 'asc' - if params.has_key(ORDER_VAR): - try: - try: - f = lookup_opts.get_field(lookup_opts.admin.list_display[int(params[ORDER_VAR])]) - except meta.FieldDoesNotExist: - pass - else: - if not isinstance(f.rel, meta.ManyToOneRel) or not f.null: - order_field = f.name - except (IndexError, ValueError): - pass # Invalid ordering specified. Just use the default. - if params.has_key(ORDER_TYPE_VAR) and params[ORDER_TYPE_VAR] in ('asc', 'desc'): - order_type = params[ORDER_TYPE_VAR] - self.order_field, self.order_type = order_field, order_type - - def get_lookup_params(self): - # Prepare the lookup parameters for the API lookup. - (params, order_field, lookup_opts, order_type, opts, query) = \ - (self.params, self.order_field, self.lookup_opts, self.order_type, self.opts, self.query) - - lookup_params = params.copy() - for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): - if lookup_params.has_key(i): - del lookup_params[i] - # If the order-by field is a field with a relationship, order by the value - # in the related table. - lookup_order_field = order_field - try: - f = lookup_opts.get_field(order_field) - except meta.FieldDoesNotExist: - pass - else: - if isinstance(lookup_opts.get_field(order_field).rel, meta.ManyToOneRel): - f = lookup_opts.get_field(order_field) - rel_ordering = f.rel.to.ordering and f.rel.to.ordering[0] or f.rel.to.pk.column - lookup_order_field = '%s.%s' % (f.rel.to.db_table, rel_ordering) - # Use select_related if one of the list_display options is a field with a - # relationship. - if lookup_opts.admin.list_select_related: - lookup_params['select_related'] = True + myappend(mychr(myatoi(item[:2], 16)) + + item[2:]) + except ValueError: + myappend('_' + item) else: - for field_name in lookup_opts.admin.list_display: - try: - f = lookup_opts.get_field(field_name) - except meta.FieldDoesNotExist: - pass - else: - if isinstance(f.rel, meta.ManyToOneRel): - lookup_params['select_related'] = True - break - lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,) - if lookup_opts.admin.search_fields and query: - complex_queries = [] - for bit in query.split(): - or_queries = [] - for field_name in lookup_opts.admin.search_fields: - or_queries.append(meta.Q(**{'%s__icontains' % field_name: bit})) - complex_queries.append(reduce(operator.or_, or_queries)) - lookup_params['complex'] = reduce(operator.and_, complex_queries) - if opts.one_to_one_field: - lookup_params.update(opts.one_to_one_field.rel.limit_choices_to) - self.lookup_params = lookup_params - -def change_list(request, app_label, module_name): - try: - cl = ChangeList(request, app_label, module_name) - except IncorrectLookupParameters: - return HttpResponseRedirect(request.path) - - c = Context(request, { - 'title': cl.title, - 'is_popup': cl.is_popup, - 'cl' : cl - }) - c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}), - return render_to_response(['admin/%s/%s/change_list' % (app_label, cl.opts.object_name.lower()), - 'admin/%s/change_list' % app_label, - 'admin/change_list'], context_instance=c) -change_list = staff_member_required(change_list) - -use_raw_id_admin = lambda field: isinstance(field.rel, (meta.ManyToOneRel, meta.ManyToManyRel)) and field.rel.raw_id_admin + myappend('_' + item) + return "".join(res) -def get_javascript_imports(opts,auto_populated_fields, ordered_objects, field_sets): +def get_javascript_imports(opts, auto_populated_fields, field_sets): # Put in any necessary JavaScript imports. js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] if auto_populated_fields: js.append('js/urlify.js') - if opts.has_field_type(meta.DateTimeField) or opts.has_field_type(meta.TimeField) or opts.has_field_type(meta.DateField): + if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField): js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js']) - if ordered_objects: + if opts.get_ordered_objects(): js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) if opts.admin.js: js.extend(opts.admin.js) @@ -264,29 +96,30 @@ def get_javascript_imports(opts,auto_populated_fields, ordered_objects, field_se for field_set in field_sets: if not seen_collapse and 'collapse' in field_set.classes: seen_collapse = True - js.append('js/admin/CollapsedFieldsets.js' ) + js.append('js/admin/CollapsedFieldsets.js') for field_line in field_set: try: for f in field_line: - if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface: + if f.rel and isinstance(f, models.ManyToManyField) and f.rel.filter_interface: js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) raise StopIteration except StopIteration: break return js -class AdminBoundField(BoundField): +class AdminBoundField(object): def __init__(self, field, field_mapping, original): - super(AdminBoundField, self).__init__(field, field_mapping, original) - + self.field = field + self.original = original + self.form_fields = [field_mapping[name] for name in self.field.get_manipulator_field_names('')] self.element_id = self.form_fields[0].get_id() - self.has_label_first = not isinstance(self.field, meta.BooleanField) + self.has_label_first = not isinstance(self.field, models.BooleanField) self.raw_id_admin = use_raw_id_admin(field) - self.is_date_time = isinstance(field, meta.DateTimeField) - self.is_file_field = isinstance(field, meta.FileField) - self.needs_add_label = field.rel and isinstance(field.rel, meta.ManyToOneRel) or isinstance(field.rel, meta.ManyToManyRel) and field.rel.to.admin - self.hidden = isinstance(self.field, meta.AutoField) + self.is_date_time = isinstance(field, models.DateTimeField) + self.is_file_field = isinstance(field, models.FileField) + self.needs_add_label = field.rel and isinstance(field.rel, models.ManyToOneRel) or isinstance(field.rel, models.ManyToManyRel) and field.rel.to._meta.admin + self.hidden = isinstance(self.field, models.AutoField) self.first = False classes = [] @@ -298,26 +131,22 @@ class AdminBoundField(BoundField): self.cell_class_attribute = ' class="%s" ' % ' '.join(classes) self._repr_filled = False - def _fetch_existing_display(self, func_name): - class_dict = self.original.__class__.__dict__ - func = class_dict.get(func_name) - return func(self.original) + if field.rel: + self.related_url = '../../../%s/%s/' % (field.rel.to._meta.app_label, field.rel.to._meta.object_name.lower()) - def _fill_existing_display(self): - if getattr(self, '_display_filled', False): - return - # HACK - if isinstance(self.field.rel, meta.ManyToOneRel): - func_name = 'get_%s' % self.field.name - self._display = self._fetch_existing_display(func_name) - elif isinstance(self.field.rel, meta.ManyToManyRel): - func_name = 'get_%s_list' % self.field.rel.singular - self._display = ", ".join([str(obj) for obj in self._fetch_existing_display(func_name)]) - self._display_filled = True + def original_value(self): + if self.original: + return self.original.__dict__[self.field.column] def existing_display(self): - self._fill_existing_display() - return self._display + try: + return self._display + except AttributeError: + if isinstance(self.field.rel, models.ManyToOneRel): + self._display = getattr(self.original, 'get_%s' % self.field.name)() + elif isinstance(self.field.rel, models.ManyToManyRel): + self._display = ", ".join([str(obj) for obj in getattr(self.original, 'get_%s_list' % self.field.rel.singular)()]) + return self._display def __repr__(self): return repr(self.__dict__) @@ -325,94 +154,114 @@ class AdminBoundField(BoundField): def html_error_list(self): return " ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors]) -class AdminBoundFieldLine(BoundFieldLine): + def original_url(self): + if self.is_file_field and self.original and self.field.attname: + url_method = getattr(self.original, 'get_%s_url' % self.field.attname) + if callable(url_method): + return url_method() + return '' + +class AdminBoundFieldLine(object): def __init__(self, field_line, field_mapping, original): - super(AdminBoundFieldLine, self).__init__(field_line, field_mapping, original, AdminBoundField) + self.bound_fields = [field.bind(field_mapping, original, AdminBoundField) for field in field_line] for bound_field in self: bound_field.first = True break -class AdminBoundFieldSet(BoundFieldSet): - def __init__(self, field_set, field_mapping, original): - super(AdminBoundFieldSet, self).__init__(field_set, field_mapping, original, AdminBoundFieldLine) - -class BoundManipulator(object): - def __init__(self, opts, manipulator, field_mapping): - self.inline_related_objects = opts.get_followed_related_objects(manipulator.follow) - self.original = hasattr(manipulator, 'original_object') and manipulator.original_object or None - self.bound_field_sets = [field_set.bind(field_mapping, self.original, AdminBoundFieldSet) - for field_set in opts.admin.get_field_sets(opts)] - self.ordered_objects = opts.get_ordered_objects()[:] - -class AdminBoundManipulator(BoundManipulator): - def __init__(self, opts, manipulator, field_mapping): - super(AdminBoundManipulator, self).__init__(opts, manipulator, field_mapping) - field_sets = opts.admin.get_field_sets(opts) - - self.auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] - self.javascript_imports = get_javascript_imports(opts, self.auto_populated_fields, self.ordered_objects, field_sets); - - self.coltype = self.ordered_objects and 'colMS' or 'colM' - self.has_absolute_url = hasattr(opts.get_model_module().Klass, 'get_absolute_url') - self.form_enc_attrib = opts.has_field_type(meta.FileField) and \ - 'enctype="multipart/form-data" ' or '' + def __iter__(self): + for bound_field in self.bound_fields: + yield bound_field - self.first_form_field_id = self.bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id(); - self.ordered_object_pk_names = [o.pk.name for o in self.ordered_objects] + def __len__(self): + return len(self.bound_fields) - self.save_on_top = opts.admin.save_on_top - self.save_as = opts.admin.save_as +class AdminBoundFieldSet(object): + def __init__(self, field_set, field_mapping, original): + self.name = field_set.name + self.classes = field_set.classes + self.description = field_set.description + self.bound_field_lines = [field_line.bind(field_mapping, original, AdminBoundFieldLine) for field_line in field_set] - self.content_type_id = opts.get_content_type_id() - self.verbose_name_plural = opts.verbose_name_plural - self.verbose_name = opts.verbose_name - self.object_name = opts.object_name + def __iter__(self): + for bound_field_line in self.bound_field_lines: + yield bound_field_line - def get_ordered_object_pk(self, ordered_obj): - for name in self.ordered_object_pk_names: - if hasattr(ordered_obj, name): - return str(getattr(ordered_obj, name)) - return "" + def __len__(self): + return len(self.bound_field_lines) -def render_change_form(opts, manipulator, app_label, context, add=False, change=False, show_delete=False, form_url=''): +def render_change_form(model, manipulator, context, add=False, change=False, form_url=''): + opts = model._meta + app_label = opts.app_label + auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] + field_sets = opts.admin.get_field_sets(opts) + original = getattr(manipulator, 'original_object', None) + bound_field_sets = [field_set.bind(context['form'], original, AdminBoundFieldSet) for field_set in field_sets] + first_form_field_id = bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id(); + ordered_objects = opts.get_ordered_objects() + inline_related_objects = opts.get_followed_related_objects(manipulator.follow) extra_context = { 'add': add, 'change': change, - 'bound_manipulator': AdminBoundManipulator(opts, manipulator, context['form']), 'has_delete_permission': context['perms'][app_label][opts.get_delete_permission()], + 'has_change_permission': context['perms'][app_label][opts.get_change_permission()], + 'has_file_field': opts.has_field_type(models.FileField), + 'has_absolute_url': hasattr(model, 'get_absolute_url'), + 'auto_populated_fields': auto_populated_fields, + 'bound_field_sets': bound_field_sets, + 'first_form_field_id': first_form_field_id, + 'javascript_imports': get_javascript_imports(opts, auto_populated_fields, field_sets), + 'ordered_objects': ordered_objects, + 'inline_related_objects': inline_related_objects, 'form_url': form_url, - 'app_label': app_label, + 'opts': opts, + 'content_type_id': ContentType.objects.get_for_model(model).id, } context.update(extra_context) - return render_to_response(["admin/%s/%s/change_form" % (app_label, opts.object_name.lower() ), - "admin/%s/change_form" % app_label , - "admin/change_form"], context_instance=context) + return render_to_response([ + "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), + "admin/%s/change_form.html" % app_label, + "admin/change_form.html"], context_instance=context) + +def index(request): + return render_to_response('admin/index.html', {'title': _('Site administration')}, context_instance=template.RequestContext(request)) +index = staff_member_required(never_cache(index)) -def log_add_message(user, opts,manipulator,new_object): - pk_value = getattr(new_object, opts.pk.attname) - log.log_action(user.id, opts.get_content_type_id(), pk_value, str(new_object), log.ADDITION) +def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None): + model = models.get_model(app_label, model_name) + if model is None: + raise Http404, "App %r, model %r, not found" % (app_label, model_name) + opts = model._meta -def add_stage(request, app_label, module_name, show_delete=False, form_url='', post_url='../', post_url_continue='../%s/', object_id_override=None): - mod, opts = _get_mod_opts(app_label, module_name) if not request.user.has_perm(app_label + '.' + opts.get_add_permission()): raise PermissionDenied - manipulator = mod.AddManipulator() + + if post_url is None: + if request.user.has_perm(app_label + '.' + opts.get_change_permission()): + # redirect to list view + post_url = '../' + else: + # Object list will give 'Permission Denied', so go back to admin home + post_url = '../../../' + + manipulator = model.AddManipulator() if request.POST: new_data = request.POST.copy() - if opts.has_field_type(meta.FileField): + + if opts.has_field_type(models.FileField): new_data.update(request.FILES) + errors = manipulator.get_validation_errors(new_data) manipulator.do_html2python(new_data) - if not errors and not request.POST.has_key("_preview"): + if not errors: new_object = manipulator.save(new_data) - log_add_message(request.user, opts,manipulator,new_object) - msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name':opts.verbose_name, 'obj':new_object} - pk_value = getattr(new_object,opts.pk.attname) + pk_value = new_object._get_pk_val() + LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, str(new_object), ADDITION) + msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object} # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. if request.POST.has_key("_continue"): - request.user.add_message(msg + ' ' + _("You may edit it again below.")) + request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) if request.POST.has_key("_popup"): post_url_continue += "?_popup=1" return HttpResponseRedirect(post_url_continue % pk_value) @@ -420,10 +269,10 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, %s, "%s");</script>' % \ (pk_value, str(new_object).replace('"', '\\"'))) elif request.POST.has_key("_addanother"): - request.user.add_message(msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) + request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) return HttpResponseRedirect(request.path) else: - request.user.add_message(msg) + request.user.message_set.create(message=msg) return HttpResponseRedirect(post_url) else: # Add default data. @@ -435,73 +284,80 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p errors = {} # Populate the FormWrapper. - form = formfields.FormWrapper(manipulator, new_data, errors, edit_inline=True) + form = forms.FormWrapper(manipulator, new_data, errors) - c = Context(request, { + c = template.RequestContext(request, { 'title': _('Add %s') % opts.verbose_name, 'form': form, 'is_popup': request.REQUEST.has_key('_popup'), 'show_delete': show_delete, }) + if object_id_override is not None: c['object_id'] = object_id_override - return render_change_form(opts, manipulator, app_label, c, add=True) -add_stage = staff_member_required(add_stage) + return render_change_form(model, manipulator, c, add=True) +add_stage = staff_member_required(never_cache(add_stage)) -def log_change_message(user, opts,manipulator,new_object): - pk_value = getattr(new_object, opts.pk.column) - # Construct the change message. - change_message = [] - if manipulator.fields_added: - change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and'))) - if manipulator.fields_changed: - change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and'))) - if manipulator.fields_deleted: - change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and'))) - change_message = ' '.join(change_message) - if not change_message: - change_message = _('No fields changed.') - log.log_action(user.id, opts.get_content_type_id(), pk_value, str(new_object), log.CHANGE, change_message) +def change_stage(request, app_label, model_name, object_id): + model = models.get_model(app_label, model_name) + object_id = unquote(object_id) + if model is None: + raise Http404, "App %r, model %r, not found" % (app_label, model_name) + opts = model._meta -def change_stage(request, app_label, module_name, object_id): - mod, opts = _get_mod_opts(app_label, module_name) if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): raise PermissionDenied + if request.POST and request.POST.has_key("_saveasnew"): - return add_stage(request, app_label, module_name, form_url='../add/') + return add_stage(request, app_label, model_name, form_url='../../add/') + try: - manipulator = mod.ChangeManipulator(object_id) + manipulator = model.ChangeManipulator(object_id) except ObjectDoesNotExist: raise Http404 if request.POST: new_data = request.POST.copy() - if opts.has_field_type(meta.FileField): + + if opts.has_field_type(models.FileField): new_data.update(request.FILES) errors = manipulator.get_validation_errors(new_data) - manipulator.do_html2python(new_data) - if not errors and not request.POST.has_key("_preview"): + + if not errors: new_object = manipulator.save(new_data) - log_change_message(request.user,opts,manipulator,new_object) - msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj':new_object} - pk_value = getattr(new_object,opts.pk.attname) + pk_value = new_object._get_pk_val() + + # Construct the change message. + change_message = [] + if manipulator.fields_added: + change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and'))) + if manipulator.fields_changed: + change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and'))) + if manipulator.fields_deleted: + change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and'))) + change_message = ' '.join(change_message) + if not change_message: + change_message = _('No fields changed.') + LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, str(new_object), CHANGE, change_message) + + msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj': new_object} if request.POST.has_key("_continue"): - request.user.add_message(msg + ' ' + _("You may edit it again below.")) + request.user.message_set.create(message=msg + ' ' + _("You may edit it again below.")) if request.REQUEST.has_key('_popup'): return HttpResponseRedirect(request.path + "?_popup=1") else: return HttpResponseRedirect(request.path) elif request.POST.has_key("_saveasnew"): - request.user.add_message(_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object}) + request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object}) return HttpResponseRedirect("../%s/" % pk_value) elif request.POST.has_key("_addanother"): - request.user.add_message(msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) + request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) return HttpResponseRedirect("../add/") else: - request.user.add_message(msg) + request.user.message_set.create(message=msg) return HttpResponseRedirect("../") else: # Populate new_data with a "flattened" version of the current data. @@ -519,7 +375,7 @@ def change_stage(request, app_label, module_name, object_id): errors = {} # Populate the FormWrapper. - form = formfields.FormWrapper(manipulator, new_data, errors, edit_inline = True) + form = forms.FormWrapper(manipulator, new_data, errors) form.original = manipulator.original_object form.order_objects = [] @@ -528,20 +384,19 @@ def change_stage(request, app_label, module_name, object_id): wrt = related.opts.order_with_respect_to if wrt and wrt.rel and wrt.rel.to == opts: func = getattr(manipulator.original_object, 'get_%s_list' % - related.get_method_name_part()) + related.get_accessor_name()) orig_list = func() form.order_objects.extend(orig_list) - c = Context(request, { + c = template.RequestContext(request, { 'title': _('Change %s') % opts.verbose_name, 'form': form, 'object_id': object_id, 'original': manipulator.original_object, - 'is_popup' : request.REQUEST.has_key('_popup') + 'is_popup': request.REQUEST.has_key('_popup'), }) - - return render_change_form(opts,manipulator, app_label, c, change=True) -change_stage = staff_member_required(change_stage) + return render_change_form(model, manipulator, c, change=True) +change_stage = staff_member_required(never_cache(change_stage)) def _nest_help(obj, depth, val): current = obj @@ -559,10 +414,10 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current if related.opts in opts_seen: continue opts_seen.append(related.opts) - rel_opts_name = related.get_method_name_part() - if isinstance(related.field.rel, meta.OneToOneRel): + rel_opts_name = related.get_accessor_name() + if isinstance(related.field.rel, models.OneToOneRel): try: - sub_obj = getattr(obj, 'get_%s' % rel_opts_name)() + sub_obj = getattr(obj, rel_opts_name) except ObjectDoesNotExist: pass else: @@ -579,12 +434,12 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current else: # Display a link to the admin page. nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ - (capfirst(related.opts.verbose_name), related.opts.app_label, related.opts.module_name, - getattr(sub_obj, related.opts.pk.attname), sub_obj), []]) + (capfirst(related.opts.verbose_name), related.opts.app_label, related.opts.object_name.lower(), + sub_obj._get_pk_val(), sub_obj), []]) _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2) else: has_related_objs = False - for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)(): + for sub_obj in getattr(obj, rel_opts_name).all(): has_related_objs = True if related.field.rel.edit_inline or not related.opts.admin: # Don't display link to edit, because it either has no @@ -593,7 +448,7 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current else: # Display a link to the admin page. nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%s</a>' % \ - (capfirst(related.opts.verbose_name), related.opts.app_label, related.opts.module_name, getattr(sub_obj, related.opts.pk.attname), escape(str(sub_obj))), []]) + (capfirst(related.opts.verbose_name), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(str(sub_obj))), []]) _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2) # If there were related objects, and the user doesn't have # permission to delete them, add the missing perm to perms_needed. @@ -605,21 +460,21 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current if related.opts in opts_seen: continue opts_seen.append(related.opts) - rel_opts_name = related.get_method_name_part() + rel_opts_name = related.get_accessor_name() has_related_objs = False - for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)(): + for sub_obj in getattr(obj, rel_opts_name).all(): has_related_objs = True if related.field.rel.edit_inline or not related.opts.admin: # Don't display link to edit, because it either has no # admin or is edited inline. nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \ - {'fieldname': related.field.name, 'name': related.opts.verbose_name, 'obj': escape(str(sub_obj))}, []]) + {'fieldname': related.field.verbose_name, 'name': related.opts.verbose_name, 'obj': escape(str(sub_obj))}, []]) else: # Display a link to the admin page. nh(deleted_objects, current_depth, [ - (_('One or more %(fieldname)s in %(name)s:') % {'fieldname': related.field.name, 'name':related.opts.verbose_name}) + \ + (_('One or more %(fieldname)s in %(name)s:') % {'fieldname': related.field.verbose_name, 'name':related.opts.verbose_name}) + \ (' <a href="../../../../%s/%s/%s/">%s</a>' % \ - (related.opts.app_label, related.opts.module_name, getattr(sub_obj, related.opts.pk.attname), escape(str(sub_obj)))), []]) + (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(str(sub_obj)))), []]) # If there were related objects, and the user doesn't have # permission to change them, add the missing perm to perms_needed. if related.opts.admin and has_related_objs: @@ -627,12 +482,16 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current if not user.has_perm(p): perms_needed.add(related.opts.verbose_name) -def delete_stage(request, app_label, module_name, object_id): +def delete_stage(request, app_label, model_name, object_id): import sets - mod, opts = _get_mod_opts(app_label, module_name) + model = models.get_model(app_label, model_name) + object_id = unquote(object_id) + if model is None: + raise Http404, "App %r, model %r, not found" % (app_label, model_name) + opts = model._meta if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()): raise PermissionDenied - obj = get_object_or_404(mod, pk=object_id) + obj = get_object_or_404(model, pk=object_id) # Populate deleted_objects, a data structure of all related objects that # will also be deleted. @@ -645,28 +504,240 @@ def delete_stage(request, app_label, module_name, object_id): raise PermissionDenied obj_display = str(obj) obj.delete() - log.log_action(request.user.id, opts.get_content_type_id(), object_id, obj_display, log.DELETION) - request.user.add_message(_('The %(name)s "%(obj)s" was deleted successfully.') % {'name':opts.verbose_name, 'obj':obj_display}) + LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, object_id, obj_display, DELETION) + request.user.message_set.create(message=_('The %(name)s "%(obj)s" was deleted successfully.') % {'name': opts.verbose_name, 'obj': obj_display}) return HttpResponseRedirect("../../") - return render_to_response('admin/delete_confirmation', { + extra_context = { "title": _("Are you sure?"), "object_name": opts.verbose_name, "object": obj, "deleted_objects": deleted_objects, "perms_lacking": perms_needed, - }, context_instance=Context(request)) -delete_stage = staff_member_required(delete_stage) + "opts": model._meta, + } + return render_to_response(["admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower() ), + "admin/%s/delete_confirmation.html" % app_label , + "admin/delete_confirmation.html"], extra_context, context_instance=template.RequestContext(request)) +delete_stage = staff_member_required(never_cache(delete_stage)) -def history(request, app_label, module_name, object_id): - mod, opts = _get_mod_opts(app_label, module_name) - action_list = log.get_list(object_id__exact=object_id, content_type__id__exact=opts.get_content_type_id(), - order_by=("action_time",), select_related=True) +def history(request, app_label, model_name, object_id): + model = models.get_model(app_label, model_name) + object_id = unquote(object_id) + if model is None: + raise Http404, "App %r, model %r, not found" % (app_label, model_name) + action_list = LogEntry.objects.filter(object_id=object_id, + content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time') # If no history was found, see whether this object even exists. - obj = get_object_or_404(mod, pk=object_id) - return render_to_response('admin/object_history', { + obj = get_object_or_404(model, pk=object_id) + extra_context = { 'title': _('Change history: %s') % obj, 'action_list': action_list, - 'module_name': capfirst(opts.verbose_name_plural), + 'module_name': capfirst(model._meta.verbose_name_plural), 'object': obj, - }, context_instance=Context(request)) -history = staff_member_required(history) + } + return render_to_response(["admin/%s/%s/object_history.html" % (app_label, model._meta.object_name.lower()), + "admin/%s/object_history.html" % app_label , + "admin/object_history.html"], extra_context, context_instance=template.RequestContext(request)) +history = staff_member_required(never_cache(history)) + +class ChangeList(object): + def __init__(self, request, model): + self.model = model + self.opts = model._meta + self.lookup_opts = self.opts + self.manager = self.opts.admin.manager + + # Get search parameters from the query string. + try: + self.page_num = int(request.GET.get(PAGE_VAR, 0)) + except ValueError: + self.page_num = 0 + self.show_all = request.GET.has_key(ALL_VAR) + self.is_popup = request.GET.has_key(IS_POPUP_VAR) + self.params = dict(request.GET.items()) + if self.params.has_key(PAGE_VAR): + del self.params[PAGE_VAR] + + self.order_field, self.order_type = self.get_ordering() + self.query = request.GET.get(SEARCH_VAR, '') + self.query_set = self.get_query_set() + self.get_results(request) + self.title = (self.is_popup and _('Select %s') % self.opts.verbose_name or _('Select %s to change') % self.opts.verbose_name) + self.filter_specs, self.has_filters = self.get_filters(request) + self.pk_attname = self.lookup_opts.pk.attname + + def get_filters(self, request): + filter_specs = [] + if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field: + filter_fields = [self.lookup_opts.get_field(field_name) \ + for field_name in self.lookup_opts.admin.list_filter] + for f in filter_fields: + spec = FilterSpec.create(f, request, self.params) + if spec and spec.has_output(): + filter_specs.append(spec) + return filter_specs, bool(filter_specs) + + def get_query_string(self, new_params={}, remove=[]): + p = self.params.copy() + for r in remove: + for k in p.keys(): + if k.startswith(r): + del p[k] + for k, v in new_params.items(): + if p.has_key(k) and v is None: + del p[k] + elif v is not None: + p[k] = v + return '?' + '&'.join(['%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20') + + def get_results(self, request): + paginator = ObjectPaginator(self.query_set, self.lookup_opts.admin.list_per_page) + + # Get the number of objects, with admin filters applied. + try: + result_count = paginator.hits + # Naked except! Because we don't have any other way of validating + # "params". They might be invalid if the keyword arguments are + # incorrect, or if the values are not in the correct type (which would + # result in a database error). + except: + raise IncorrectLookupParameters + + # Get the total number of objects, with no admin filters applied. + # Perform a slight optimization: Check to see whether any filters were + # given. If not, use paginator.hits to calculate the number of objects, + # because we've already done paginator.hits and the value is cached. + if isinstance(self.query_set._filters, models.Q) and not self.query_set._filters.kwargs: + full_result_count = result_count + else: + full_result_count = self.manager.count() + + can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED + multi_page = result_count > self.lookup_opts.admin.list_per_page + + # Get the list of objects to display on this page. + if (self.show_all and can_show_all) or not multi_page: + result_list = list(self.query_set) + else: + try: + result_list = paginator.get_page(self.page_num) + except InvalidPage: + result_list = () + + self.result_count = result_count + self.full_result_count = full_result_count + self.result_list = result_list + self.can_show_all = can_show_all + self.multi_page = multi_page + self.paginator = paginator + + def get_ordering(self): + lookup_opts, params = self.lookup_opts, self.params + # For ordering, first check the "ordering" parameter in the admin options, + # then check the object's default ordering. If neither of those exist, + # order descending by ID by default. Finally, look for manually-specified + # ordering from the query string. + ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] + + # Normalize it to new-style ordering. + ordering = handle_legacy_orderlist(ordering) + + if ordering[0].startswith('-'): + order_field, order_type = ordering[0][1:], 'desc' + else: + order_field, order_type = ordering[0], 'asc' + if params.has_key(ORDER_VAR): + try: + try: + f = lookup_opts.get_field(lookup_opts.admin.list_display[int(params[ORDER_VAR])]) + except models.FieldDoesNotExist: + pass + else: + if not isinstance(f.rel, models.ManyToOneRel) or not f.null: + order_field = f.name + except (IndexError, ValueError): + pass # Invalid ordering specified. Just use the default. + if params.has_key(ORDER_TYPE_VAR) and params[ORDER_TYPE_VAR] in ('asc', 'desc'): + order_type = params[ORDER_TYPE_VAR] + return order_field, order_type + + def get_query_set(self): + qs = self.manager.get_query_set() + lookup_params = self.params.copy() # a dictionary of the query string + for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): + if lookup_params.has_key(i): + del lookup_params[i] + + # Apply lookup parameters from the query string. + qs = qs.filter(**lookup_params) + + # Use select_related() if one of the list_display options is a field + # with a relationship. + if self.lookup_opts.admin.list_select_related: + qs = qs.select_related() + else: + for field_name in self.lookup_opts.admin.list_display: + try: + f = self.lookup_opts.get_field(field_name) + except models.FieldDoesNotExist: + pass + else: + if isinstance(f.rel, models.ManyToOneRel): + qs = qs.select_related() + break + + # Calculate lookup_order_field. + # If the order-by field is a field with a relationship, order by the + # value in the related table. + lookup_order_field = self.order_field + try: + f = self.lookup_opts.get_field(self.order_field, many_to_many=False) + except models.FieldDoesNotExist: + pass + else: + if isinstance(f.rel, models.OneToOneRel): + # For OneToOneFields, don't try to order by the related object's ordering criteria. + pass + elif isinstance(f.rel, models.ManyToOneRel): + rel_ordering = f.rel.to._meta.ordering and f.rel.to._meta.ordering[0] or f.rel.to._meta.pk.column + lookup_order_field = '%s.%s' % (f.rel.to._meta.db_table, rel_ordering) + + # Set ordering. + qs = qs.order_by((self.order_type == 'desc' and '-' or '') + lookup_order_field) + + # Apply keyword searches. + if self.lookup_opts.admin.search_fields and self.query: + for bit in self.query.split(): + or_queries = [models.Q(**{'%s__icontains' % field_name: bit}) for field_name in self.lookup_opts.admin.search_fields] + other_qs = QuerySet(self.model) + other_qs = other_qs.filter(reduce(operator.or_, or_queries)) + qs = qs & other_qs + + if self.opts.one_to_one_field: + qs = qs.filter(**self.opts.one_to_one_field.rel.limit_choices_to) + + return qs + + def url_for_result(self, result): + return "%s/" % quote(getattr(result, self.pk_attname)) + +def change_list(request, app_label, model_name): + model = models.get_model(app_label, model_name) + if model is None: + raise Http404, "App %r, model %r, not found" % (app_label, model_name) + if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()): + raise PermissionDenied + try: + cl = ChangeList(request, model) + except IncorrectLookupParameters: + return HttpResponseRedirect(request.path) + c = template.RequestContext(request, { + 'title': cl.title, + 'is_popup': cl.is_popup, + 'cl': cl, + }) + c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}), + return render_to_response(['admin/%s/%s/change_list.html' % (app_label, cl.opts.object_name.lower()), + 'admin/%s/change_list.html' % app_label, + 'admin/change_list.html'], context_instance=c) +change_list = staff_member_required(never_cache(change_list)) diff --git a/django/contrib/admin/views/template.py b/django/contrib/admin/views/template.py index 3effd57c10..f73b9e4218 100644 --- a/django/contrib/admin/views/template.py +++ b/django/contrib/admin/views/template.py @@ -1,9 +1,9 @@ from django.contrib.admin.views.decorators import staff_member_required -from django.core import formfields, validators -from django.core import template -from django.core.template import loader -from django.core.extensions import DjangoContext, render_to_response -from django.models.core import sites +from django.core import validators +from django import template, forms +from django.template import loader +from django.shortcuts import render_to_response +from django.contrib.sites.models import Site from django.conf import settings def template_validator(request): @@ -23,19 +23,19 @@ def template_validator(request): errors = manipulator.get_validation_errors(new_data) if not errors: request.user.add_message('The template is valid.') - return render_to_response('admin/template_validator', { + return render_to_response('admin/template_validator.html', { 'title': 'Template validator', - 'form': formfields.FormWrapper(manipulator, new_data, errors), - }, context_instance=DjangoContext(request)) + 'form': forms.FormWrapper(manipulator, new_data, errors), + }, context_instance=template.RequestContext(request)) template_validator = staff_member_required(template_validator) -class TemplateValidator(formfields.Manipulator): +class TemplateValidator(forms.Manipulator): def __init__(self, settings_modules): self.settings_modules = settings_modules - site_list = sites.get_in_bulk(settings_modules.keys()).values() + site_list = Site.objects.get_in_bulk(settings_modules.keys()).values() self.fields = ( - formfields.SelectField('site', is_required=True, choices=[(s.id, s.name) for s in site_list]), - formfields.LargeTextField('template', is_required=True, rows=25, validator_list=[self.isValidTemplate]), + forms.SelectField('site', is_required=True, choices=[(s.id, s.name) for s in site_list]), + forms.LargeTextField('template', is_required=True, rows=25, validator_list=[self.isValidTemplate]), ) def isValidTemplate(self, field_data, all_data): diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index e69de29bb2..ac7b40aca6 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -0,0 +1,2 @@ +LOGIN_URL = '/accounts/login/' +REDIRECT_FIELD_NAME = 'next' diff --git a/django/contrib/auth/create_superuser.py b/django/contrib/auth/create_superuser.py new file mode 100644 index 0000000000..ab5ca36f50 --- /dev/null +++ b/django/contrib/auth/create_superuser.py @@ -0,0 +1,84 @@ +""" +Helper function for creating superusers in the authentication system. +""" + +from django.core import validators +from django.contrib.auth.models import User +import getpass +import os +import sys + +def createsuperuser(username=None, email=None, password=None): + """ + Helper function for creating a superuser from the command line. All + arguments are optional and will be prompted-for if invalid or not given. + """ + try: + import pwd + except ImportError: + default_username = '' + else: + # Determine the current system user's username, to use as a default. + default_username = pwd.getpwuid(os.getuid())[0].replace(' ', '').lower() + + # Determine whether the default username is taken, so we don't display + # it as an option. + if default_username: + try: + User.objects.get(username=default_username) + except User.DoesNotExist: + pass + else: + default_username = '' + + try: + while 1: + if not username: + input_msg = 'Username' + if default_username: + input_msg += ' (Leave blank to use %r)' % default_username + username = raw_input(input_msg + ': ') + if default_username and username == '': + username = default_username + if not username.isalnum(): + sys.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.\n") + username = None + try: + User.objects.get(username=username) + except User.DoesNotExist: + break + else: + sys.stderr.write("Error: That username is already taken.\n") + username = None + while 1: + if not email: + email = raw_input('E-mail address: ') + try: + validators.isValidEmail(email, None) + except validators.ValidationError: + sys.stderr.write("Error: That e-mail address is invalid.\n") + email = None + else: + break + while 1: + if not password: + password = getpass.getpass() + password2 = getpass.getpass('Password (again): ') + if password != password2: + sys.stderr.write("Error: Your passwords didn't match.\n") + password = None + continue + if password.strip() == '': + sys.stderr.write("Error: Blank passwords aren't allowed.\n") + password = None + continue + break + except KeyboardInterrupt: + sys.stderr.write("\nOperation cancelled.\n") + sys.exit(1) + u = User.objects.create_user(username, email, password) + u.is_staff = True + u.is_active = True + u.is_superuser = True + u.save() + print "Superuser created successfully." diff --git a/django/views/decorators/auth.py b/django/contrib/auth/decorators.py index 478f0ac84d..4b264cf815 100644 --- a/django/views/decorators/auth.py +++ b/django/contrib/auth/decorators.py @@ -1,6 +1,7 @@ -from django.views.auth import login +from django.contrib.auth import LOGIN_URL, REDIRECT_FIELD_NAME +from django.http import HttpResponseRedirect -def user_passes_test(test_func, login_url=login.LOGIN_URL): +def user_passes_test(test_func, login_url=LOGIN_URL): """ Decorator for views that checks that the user passes the given test, redirecting to the log-in page if necessary. The test should be a callable @@ -10,7 +11,8 @@ def user_passes_test(test_func, login_url=login.LOGIN_URL): def _checklogin(request, *args, **kwargs): if test_func(request.user): return view_func(request, *args, **kwargs) - return login.redirect_to_login(request.path, login_url) + return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.path)) + return _checklogin return _dec diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py new file mode 100644 index 0000000000..6c0c8abe97 --- /dev/null +++ b/django/contrib/auth/forms.py @@ -0,0 +1,108 @@ +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.template import Context, loader +from django.core import validators +from django import forms + +class AuthenticationForm(forms.Manipulator): + """ + Base class for authenticating users. Extend this to get a form that accepts + username/password logins. + """ + def __init__(self, request=None): + """ + If request is passed in, the manipulator will validate that cookies are + enabled. Note that the request (a HttpRequest object) must have set a + cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before + running this validator. + """ + self.request = request + self.fields = [ + forms.TextField(field_name="username", length=15, maxlength=30, is_required=True, + validator_list=[self.isValidUser, self.hasCookiesEnabled]), + forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True, + validator_list=[self.isValidPasswordForUser]), + ] + self.user_cache = None + + def hasCookiesEnabled(self, field_data, all_data): + if self.request and not self.request.session.test_cookie_worked(): + raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") + + def isValidUser(self, field_data, all_data): + try: + self.user_cache = User.objects.get(username=field_data) + except User.DoesNotExist: + raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") + + def isValidPasswordForUser(self, field_data, all_data): + if self.user_cache is not None and not self.user_cache.check_password(field_data): + self.user_cache = None + raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") + + def get_user_id(self): + if self.user_cache: + return self.user_cache.id + return None + + def get_user(self): + return self.user_cache + +class PasswordResetForm(forms.Manipulator): + "A form that lets a user request a password reset" + def __init__(self): + self.fields = ( + forms.EmailField(field_name="email", length=40, is_required=True, + validator_list=[self.isValidUserEmail]), + ) + + def isValidUserEmail(self, new_data, all_data): + "Validates that a user exists with the given e-mail address" + try: + self.user_cache = User.objects.get(email__iexact=new_data) + except User.DoesNotExist: + raise validators.ValidationError, "That e-mail address doesn't have an associated user acount. Are you sure you've registered?" + + def save(self, domain_override=None): + "Calculates a new password randomly and sends it to the user" + from django.core.mail import send_mail + new_pass = User.objects.make_random_password() + self.user_cache.set_password(new_pass) + self.user_cache.save() + if not domain_override: + current_site = Site.objects.get_current() + site_name = current_site.name + domain = current_site.domain + else: + site_name = domain = domain_override + t = loader.get_template('registration/password_reset_email.html') + c = { + 'new_password': new_pass, + 'email': self.user_cache.email, + 'domain': domain, + 'site_name': site_name, + 'user': self.user_cache, + } + send_mail('Password reset on %s' % site_name, t.render(Context(c)), None, [self.user_cache.email]) + +class PasswordChangeForm(forms.Manipulator): + "A form that lets a user change his password." + def __init__(self, user): + self.user = user + self.fields = ( + forms.PasswordField(field_name="old_password", length=30, maxlength=30, is_required=True, + validator_list=[self.isValidOldPassword]), + forms.PasswordField(field_name="new_password1", length=30, maxlength=30, is_required=True, + validator_list=[validators.AlwaysMatchesOtherField('new_password2', "The two 'new password' fields didn't match.")]), + forms.PasswordField(field_name="new_password2", length=30, maxlength=30, is_required=True), + ) + + def isValidOldPassword(self, new_data, all_data): + "Validates that the old_password field is correct." + if not self.user.check_password(new_data): + raise validators.ValidationError, "Your old password was entered incorrectly. Please enter it again." + + def save(self, new_data): + "Saves the new password." + self.user.set_password(new_data['new_password1']) + self.user.save() diff --git a/django/contrib/auth/handlers/modpython.py b/django/contrib/auth/handlers/modpython.py index d538c9ccc8..b1d7680a33 100644 --- a/django/contrib/auth/handlers/modpython.py +++ b/django/contrib/auth/handlers/modpython.py @@ -10,7 +10,7 @@ def authenhandler(req, **kwargs): # that so that the following import works os.environ.update(req.subprocess_env) - from django.models.auth import users + from django.contrib.auth.models import User # check for PythonOptions _str_to_bool = lambda s: s.lower() in ('1', 'true', 'on', 'yes') @@ -21,14 +21,14 @@ def authenhandler(req, **kwargs): superuser_only = _str_to_bool(options.get('DjangoRequireSuperuserStatus', "off")) # check that the username is valid - kwargs = {'username__exact': req.user, 'is_active__exact': True} + kwargs = {'username': req.user, 'is_active': True} if staff_only: - kwargs['is_staff__exact'] = True + kwargs['is_staff'] = True if superuser_only: - kwargs['is_superuser__exact'] = True + kwargs['is_superuser'] = True try: - user = users.get_object(**kwargs) - except users.UserDoesNotExist: + user = User.objects.get(**kwargs) + except User.DoesNotExist: return apache.HTTP_UNAUTHORIZED # check the password and any permission given diff --git a/django/contrib/auth/management.py b/django/contrib/auth/management.py new file mode 100644 index 0000000000..fe3399edbb --- /dev/null +++ b/django/contrib/auth/management.py @@ -0,0 +1,53 @@ +""" +Creates permissions for all installed apps that need permissions. +""" + +from django.dispatch import dispatcher +from django.db.models import get_models, signals +from django.contrib.auth import models as auth_app + +def _get_permission_codename(action, opts): + return '%s_%s' % (action, opts.object_name.lower()) + +def _get_all_permissions(opts): + "Returns (codename, name) for all permissions in the given opts." + perms = [] + for action in ('add', 'change', 'delete'): + perms.append((_get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name))) + return perms + list(opts.permissions) + +def create_permissions(app, created_models): + from django.contrib.contenttypes.models import ContentType + from django.contrib.auth.models import Permission + app_models = get_models(app) + if not app_models: + return + for klass in app_models: + if not klass._meta.admin: + continue + ctype = ContentType.objects.get_for_model(klass) + for codename, name in _get_all_permissions(klass._meta): + try: + Permission.objects.get(name=name, codename=codename, content_type__pk=ctype.id) + except Permission.DoesNotExist: + p = Permission(name=name, codename=codename, content_type=ctype) + p.save() + print "Adding permission '%s'" % p + +def create_superuser(app, created_models): + from django.contrib.auth.models import User + from django.contrib.auth.create_superuser import createsuperuser as do_create + if User in created_models: + msg = "\nYou just installed Django's auth system, which means you don't have " \ + "any superusers defined.\nWould you like to create one now? (yes/no): " + confirm = raw_input(msg) + while 1: + if confirm not in ('yes', 'no'): + confirm = raw_input('Please enter either "yes" or "no": ') + continue + if confirm == 'yes': + do_create() + break + +dispatcher.connect(create_permissions, signal=signals.post_syncdb) +dispatcher.connect(create_superuser, sender=auth_app, signal=signals.post_syncdb) diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py new file mode 100644 index 0000000000..4b3ed54960 --- /dev/null +++ b/django/contrib/auth/middleware.py @@ -0,0 +1,19 @@ +class LazyUser(object): + def __init__(self): + self._user = None + + def __get__(self, request, obj_type=None): + if self._user is None: + from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY + try: + user_id = request.session[SESSION_KEY] + self._user = User.objects.get(pk=user_id) + except (KeyError, User.DoesNotExist): + self._user = AnonymousUser() + return self._user + +class AuthenticationMiddleware: + def process_request(self, request): + assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." + request.__class__.user = LazyUser() + return None diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py new file mode 100644 index 0000000000..cea7a694c9 --- /dev/null +++ b/django/contrib/auth/models.py @@ -0,0 +1,264 @@ +from django.core import validators +from django.db import backend, connection, models +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext_lazy as _ +import datetime + +SESSION_KEY = '_auth_user_id' + +class SiteProfileNotAvailable(Exception): + pass + +class Permission(models.Model): + name = models.CharField(_('name'), maxlength=50) + content_type = models.ForeignKey(ContentType) + codename = models.CharField(_('codename'), maxlength=100) + class Meta: + verbose_name = _('permission') + verbose_name_plural = _('permissions') + unique_together = (('content_type', 'codename'),) + ordering = ('content_type', 'codename') + + def __str__(self): + return "%r | %s" % (self.content_type, self.name) + +class Group(models.Model): + name = models.CharField(_('name'), maxlength=80, unique=True) + permissions = models.ManyToManyField(Permission, verbose_name=_('permissions'), blank=True, filter_interface=models.HORIZONTAL) + class Meta: + verbose_name = _('group') + verbose_name_plural = _('groups') + ordering = ('name',) + class Admin: + search_fields = ('name',) + + def __str__(self): + return self.name + +class UserManager(models.Manager): + def create_user(self, username, email, password): + "Creates and saves a User with the given username, e-mail and password." + now = datetime.datetime.now() + user = self.model(None, username, '', '', email.strip().lower(), 'placeholder', False, True, False, now, now) + user.set_password(password) + user.save() + return user + + def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'): + "Generates a random password with the given length and given allowed_chars" + # Note that default value of allowed_chars does not have "I" or letters + # that look like it -- just to avoid confusion. + from random import choice + return ''.join([choice(allowed_chars) for i in range(length)]) + +class User(models.Model): + username = models.CharField(_('username'), maxlength=30, unique=True, validator_list=[validators.isAlphaNumeric]) + first_name = models.CharField(_('first name'), maxlength=30, blank=True) + last_name = models.CharField(_('last name'), maxlength=30, blank=True) + email = models.EmailField(_('e-mail address'), blank=True) + password = models.CharField(_('password'), maxlength=128, help_text=_("Use '[algo]$[salt]$[hexdigest]'")) + is_staff = models.BooleanField(_('staff status'), help_text=_("Designates whether the user can log into this admin site.")) + is_active = models.BooleanField(_('active'), default=True) + is_superuser = models.BooleanField(_('superuser status')) + last_login = models.DateTimeField(_('last login'), default=models.LazyDate()) + date_joined = models.DateTimeField(_('date joined'), default=models.LazyDate()) + groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True, + help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in.")) + user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True, filter_interface=models.HORIZONTAL) + objects = UserManager() + class Meta: + verbose_name = _('user') + verbose_name_plural = _('users') + ordering = ('username',) + class Admin: + fields = ( + (None, {'fields': ('username', 'password')}), + (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), + (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + (_('Groups'), {'fields': ('groups',)}), + ) + list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') + list_filter = ('is_staff', 'is_superuser') + search_fields = ('username', 'first_name', 'last_name', 'email') + + def __str__(self): + return self.username + + def get_absolute_url(self): + return "/users/%s/" % self.username + + def is_anonymous(self): + return False + + def get_full_name(self): + full_name = '%s %s' % (self.first_name, self.last_name) + return full_name.strip() + + def set_password(self, raw_password): + import sha, random + algo = 'sha1' + salt = sha.new(str(random.random())).hexdigest()[:5] + hsh = sha.new(salt+raw_password).hexdigest() + self.password = '%s$%s$%s' % (algo, salt, hsh) + + def check_password(self, raw_password): + """ + Returns a boolean of whether the raw_password was correct. Handles + encryption formats behind the scenes. + """ + # Backwards-compatibility check. Older passwords won't include the + # algorithm or salt. + if '$' not in self.password: + import md5 + is_correct = (self.password == md5.new(raw_password).hexdigest()) + if is_correct: + # Convert the password to the new, more secure format. + self.set_password(raw_password) + self.save() + return is_correct + algo, salt, hsh = self.password.split('$') + if algo == 'md5': + import md5 + return hsh == md5.new(salt+raw_password).hexdigest() + elif algo == 'sha1': + import sha + return hsh == sha.new(salt+raw_password).hexdigest() + raise ValueError, "Got unknown password algorithm type in password." + + def get_group_permissions(self): + "Returns a list of permission strings that this user has through his/her groups." + if not hasattr(self, '_group_perm_cache'): + import sets + cursor = connection.cursor() + # The SQL below works out to the following, after DB quoting: + # cursor.execute(""" + # SELECT ct."app_label", p."codename" + # FROM "auth_permission" p, "auth_group_permissions" gp, "auth_user_groups" ug, "django_content_type" ct + # WHERE p."id" = gp."permission_id" + # AND gp."group_id" = ug."group_id" + # AND ct."id" = p."content_type_id" + # AND ug."user_id" = %s, [self.id]) + sql = """ + SELECT ct.%s, p.%s + FROM %s p, %s gp, %s ug, %s ct + WHERE p.%s = gp.%s + AND gp.%s = ug.%s + AND ct.%s = p.%s + AND ug.%s = %%s""" % ( + backend.quote_name('app_label'), backend.quote_name('codename'), + backend.quote_name('auth_permission'), backend.quote_name('auth_group_permissions'), + backend.quote_name('auth_user_groups'), backend.quote_name('django_content_type'), + backend.quote_name('id'), backend.quote_name('permission_id'), + backend.quote_name('group_id'), backend.quote_name('group_id'), + backend.quote_name('id'), backend.quote_name('content_type_id'), + backend.quote_name('user_id'),) + cursor.execute(sql, [self.id]) + self._group_perm_cache = sets.Set(["%s.%s" % (row[0], row[1]) for row in cursor.fetchall()]) + return self._group_perm_cache + + def get_all_permissions(self): + if not hasattr(self, '_perm_cache'): + import sets + self._perm_cache = sets.Set(["%s.%s" % (p.content_type.app_label, p.codename) for p in self.user_permissions.all()]) + self._perm_cache.update(self.get_group_permissions()) + return self._perm_cache + + def has_perm(self, perm): + "Returns True if the user has the specified permission." + if not self.is_active: + return False + if self.is_superuser: + return True + return perm in self.get_all_permissions() + + def has_perms(self, perm_list): + "Returns True if the user has each of the specified permissions." + for perm in perm_list: + if not self.has_perm(perm): + return False + return True + + def has_module_perms(self, app_label): + "Returns True if the user has any permissions in the given app label." + if self.is_superuser: + return True + return bool(len([p for p in self.get_all_permissions() if p[:p.index('.')] == app_label])) + + def get_and_delete_messages(self): + messages = [] + for m in self.message_set.all(): + messages.append(m.message) + m.delete() + return messages + + def email_user(self, subject, message, from_email=None): + "Sends an e-mail to this User." + from django.core.mail import send_mail + send_mail(subject, message, from_email, [self.email]) + + def get_profile(self): + """ + Returns site-specific profile for this user. Raises + SiteProfileNotAvailable if this site does not allow profiles. + """ + if not hasattr(self, '_profile_cache'): + from django.conf import settings + if not settings.AUTH_PROFILE_MODULE: + raise SiteProfileNotAvailable + try: + app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.') + model = models.get_model(app_label, model_name) + self._profile_cache = model._default_manager.get(user__id__exact=self.id) + except ImportError, ImproperlyConfigured: + raise SiteProfileNotAvailable + return self._profile_cache + +class Message(models.Model): + user = models.ForeignKey(User) + message = models.TextField(_('message')) + + def __str__(self): + return self.message + +class AnonymousUser(object): + id = None + username = '' + + def __init__(self): + pass + + def __str__(self): + return 'AnonymousUser' + + def save(self): + raise NotImplementedError + + def delete(self): + raise NotImplementedError + + def set_password(self, raw_password): + raise NotImplementedError + + def check_password(self, raw_password): + raise NotImplementedError + + def _get_groups(self): + raise NotImplementedError + groups = property(_get_groups) + + def _get_user_permissions(self): + raise NotImplementedError + user_permissions = property(_get_user_permissions) + + def has_perm(self, perm): + return False + + def has_module_perms(self, module): + return False + + def get_and_delete_messages(self): + return [] + + def is_anonymous(self): + return True diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py new file mode 100644 index 0000000000..f919f82419 --- /dev/null +++ b/django/contrib/auth/views.py @@ -0,0 +1,84 @@ +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm +from django import forms +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.contrib.auth.models import SESSION_KEY +from django.contrib.sites.models import Site +from django.http import HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from django.contrib.auth import LOGIN_URL, REDIRECT_FIELD_NAME + +def login(request): + "Displays the login form and handles the login action." + manipulator = AuthenticationForm(request) + redirect_to = request.REQUEST.get(REDIRECT_FIELD_NAME, '') + if request.POST: + errors = manipulator.get_validation_errors(request.POST) + if not errors: + # Light security check -- make sure redirect_to isn't garbage. + if not redirect_to or '://' in redirect_to or ' ' in redirect_to: + redirect_to = '/accounts/profile/' + request.session[SESSION_KEY] = manipulator.get_user_id() + request.session.delete_test_cookie() + return HttpResponseRedirect(redirect_to) + else: + errors = {} + request.session.set_test_cookie() + return render_to_response('registration/login.html', { + 'form': forms.FormWrapper(manipulator, request.POST, errors), + REDIRECT_FIELD_NAME: redirect_to, + 'site_name': Site.objects.get_current().name, + }, context_instance=RequestContext(request)) + +def logout(request, next_page=None): + "Logs out the user and displays 'You are logged out' message." + try: + del request.session[SESSION_KEY] + except KeyError: + return render_to_response('registration/logged_out.html', {'title': 'Logged out'}, context_instance=RequestContext(request)) + else: + # Redirect to this page until the session has been cleared. + return HttpResponseRedirect(next_page or request.path) + +def logout_then_login(request, login_url=LOGIN_URL): + "Logs out the user if he is logged in. Then redirects to the log-in page." + return logout(request, login_url) + +def redirect_to_login(next, login_url=LOGIN_URL): + "Redirects the user to the login page, passing the given 'next' page" + return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, next)) + +def password_reset(request, is_admin_site=False): + new_data, errors = {}, {} + form = PasswordResetForm() + if request.POST: + new_data = request.POST.copy() + errors = form.get_validation_errors(new_data) + if not errors: + if is_admin_site: + form.save(request.META['HTTP_HOST']) + else: + form.save() + return HttpResponseRedirect('%sdone/' % request.path) + return render_to_response('registration/password_reset_form.html', {'form': forms.FormWrapper(form, new_data, errors)}, + context_instance=RequestContext(request)) + +def password_reset_done(request): + return render_to_response('registration/password_reset_done.html', context_instance=RequestContext(request)) + +def password_change(request): + new_data, errors = {}, {} + form = PasswordChangeForm(request.user) + if request.POST: + new_data = request.POST.copy() + errors = form.get_validation_errors(new_data) + if not errors: + form.save(new_data) + return HttpResponseRedirect('%sdone/' % request.path) + return render_to_response('registration/password_change_form.html', {'form': forms.FormWrapper(form, new_data, errors)}, + context_instance=RequestContext(request)) +password_change = login_required(password_change) + +def password_change_done(request): + return render_to_response('registration/password_change_done.html', context_instance=RequestContext(request)) diff --git a/django/contrib/comments/feeds.py b/django/contrib/comments/feeds.py index dd6c6ecf15..1f30a3ada2 100644 --- a/django/contrib/comments/feeds.py +++ b/django/contrib/comments/feeds.py @@ -1,48 +1,48 @@ from django.conf import settings +from django.contrib.comments.models import Comment, FreeComment from django.contrib.syndication.feeds import Feed from django.core.exceptions import ObjectDoesNotExist -from django.models.core import sites -from django.models.comments import comments, freecomments +from django.contrib.sites.models import Site class LatestFreeCommentsFeed(Feed): """Feed of latest comments on the current site""" - - comments_module = freecomments - + + comments_class = FreeComment + def title(self): if not hasattr(self, '_site'): - self._site = sites.get_current() + self._site = Site.objects.get_current() return "%s comments" % self._site.name - + def link(self): if not hasattr(self, '_site'): - self._site = sites.get_current() + self._site = Site.objects.get_current() return "http://%s/" % (self._site.domain) - + def description(self): if not hasattr(self, '_site'): - self._site = sites.get_current() + self._site = Site.objects.get_current() return "Latest comments on %s" % self._site.name def items(self): - return self.comments_module.get_list(**self._get_lookup_kwargs()) + return self.comments_class.objects.filter(**self._get_lookup_kwargs()) def _get_lookup_kwargs(self): return { - 'site__pk' : settings.SITE_ID, - 'is_public__exact' : True, - 'limit' : 40, + 'site__pk': settings.SITE_ID, + 'is_public__exact': True, + 'limit': 40, } class LatestCommentsFeed(LatestFreeCommentsFeed): """Feed of latest free comments on the current site""" - - comments_module = comments - + + comments_class = Comment + def _get_lookup_kwargs(self): kwargs = LatestFreeCommentsFeed._get_lookup_kwargs(self) kwargs['is_removed__exact'] = False if settings.COMMENTS_BANNED_USERS_GROUP: kwargs['where'] = ['user_id NOT IN (SELECT user_id FROM auth_users_group WHERE group_id = %s)'] kwargs['params'] = [COMMENTS_BANNED_USERS_GROUP] - return kwargs
\ No newline at end of file + return kwargs diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py new file mode 100644 index 0000000000..f9122b07d5 --- /dev/null +++ b/django/contrib/comments/models.py @@ -0,0 +1,285 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ +from django.conf import settings +import datetime + +MIN_PHOTO_DIMENSION = 5 +MAX_PHOTO_DIMENSION = 1000 + +# option codes for comment-form hidden fields +PHOTOS_REQUIRED = 'pr' +PHOTOS_OPTIONAL = 'pa' +RATINGS_REQUIRED = 'rr' +RATINGS_OPTIONAL = 'ra' +IS_PUBLIC = 'ip' + +# what users get if they don't have any karma +DEFAULT_KARMA = 5 +KARMA_NEEDED_BEFORE_DISPLAYED = 3 + +class CommentManager(models.Manager): + def get_security_hash(self, options, photo_options, rating_options, target): + """ + Returns the MD5 hash of the given options (a comma-separated string such as + 'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to + validate that submitted form options have not been tampered-with. + """ + import md5 + return md5.new(options + photo_options + rating_options + target + settings.SECRET_KEY).hexdigest() + + def get_rating_options(self, rating_string): + """ + Given a rating_string, this returns a tuple of (rating_range, options). + >>> s = "scale:1-10|First_category|Second_category" + >>> get_rating_options(s) + ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category']) + """ + rating_range, options = rating_string.split('|', 1) + rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1) + choices = [c.replace('_', ' ') for c in options.split('|')] + return rating_range, choices + + def get_list_with_karma(self, **kwargs): + """ + Returns a list of Comment objects matching the given lookup terms, with + _karma_total_good and _karma_total_bad filled. + """ + extra_kwargs = {} + extra_kwargs.setdefault('select', {}) + extra_kwargs['select']['_karma_total_good'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=1' + extra_kwargs['select']['_karma_total_bad'] = 'SELECT COUNT(*) FROM comments_karmascore, comments_comment WHERE comments_karmascore.comment_id=comments_comment.id AND score=-1' + return self.filter(**kwargs).extra(**extra_kwargs) + + def user_is_moderator(self, user): + if user.is_superuser: + return True + for g in user.get_group_list(): + if g.id == settings.COMMENTS_MODERATORS_GROUP: + return True + return False + +class Comment(models.Model): + user = models.ForeignKey(User, raw_id_admin=True) + content_type = models.ForeignKey(ContentType) + object_id = models.IntegerField(_('object ID')) + headline = models.CharField(_('headline'), maxlength=255, blank=True) + comment = models.TextField(_('comment'), maxlength=3000) + rating1 = models.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True) + rating2 = models.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True) + rating3 = models.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True) + rating4 = models.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True) + rating5 = models.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True) + rating6 = models.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True) + rating7 = models.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True) + rating8 = models.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True) + # This field designates whether to use this row's ratings in aggregate + # functions (summaries). We need this because people are allowed to post + # multiple reviews on the same thing, but the system will only use the + # latest one (with valid_rating=True) in tallying the reviews. + valid_rating = models.BooleanField(_('is valid rating')) + submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True) + is_public = models.BooleanField(_('is public')) + ip_address = models.IPAddressField(_('IP address'), blank=True, null=True) + is_removed = models.BooleanField(_('is removed'), help_text=_('Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.')) + site = models.ForeignKey(Site) + objects = CommentManager() + class Meta: + verbose_name = _('comment') + verbose_name_plural = _('comments') + ordering = ('-submit_date',) + class Admin: + fields = ( + (None, {'fields': ('content_type', 'object_id', 'site')}), + ('Content', {'fields': ('user', 'headline', 'comment')}), + ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), + ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), + ) + list_display = ('user', 'submit_date', 'content_type', 'get_content_object') + list_filter = ('submit_date',) + date_hierarchy = 'submit_date' + search_fields = ('comment', 'user__username') + + def __repr__(self): + return "%s: %s..." % (self.user.username, self.comment[:100]) + + def get_absolute_url(self): + return self.get_content_object().get_absolute_url() + "#c" + str(self.id) + + def get_crossdomain_url(self): + return "/r/%d/%d/" % (self.content_type_id, self.object_id) + + def get_flag_url(self): + return "/comments/flag/%s/" % self.id + + def get_deletion_url(self): + return "/comments/delete/%s/" % self.id + + def get_content_object(self): + """ + Returns the object that this comment is a comment on. Returns None if + the object no longer exists. + """ + from django.core.exceptions import ObjectDoesNotExist + try: + return self.get_content_type().get_object_for_this_type(pk=self.object_id) + except ObjectDoesNotExist: + return None + + get_content_object.short_description = _('Content object') + + def _fill_karma_cache(self): + "Helper function that populates good/bad karma caches" + good, bad = 0, 0 + for k in self.get_karmascore_list(): + if k.score == -1: + bad +=1 + elif k.score == 1: + good +=1 + self._karma_total_good, self._karma_total_bad = good, bad + + def get_good_karma_total(self): + if not hasattr(self, "_karma_total_good"): + self._fill_karma_cache() + return self._karma_total_good + + def get_bad_karma_total(self): + if not hasattr(self, "_karma_total_bad"): + self._fill_karma_cache() + return self._karma_total_bad + + def get_karma_total(self): + if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"): + self._fill_karma_cache() + return self._karma_total_good + self._karma_total_bad + + def get_as_text(self): + return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \ + {'user': self.user.username, 'date': self.submit_date, + 'comment': self.comment, 'domain': self.get_site().domain, 'url': self.get_absolute_url()} + +class FreeComment(models.Model): + # A FreeComment is a comment by a non-registered user. + content_type = models.ForeignKey(ContentType) + object_id = models.IntegerField(_('object ID')) + comment = models.TextField(_('comment'), maxlength=3000) + person_name = models.CharField(_("person's name"), maxlength=50) + submit_date = models.DateTimeField(_('date/time submitted'), auto_now_add=True) + is_public = models.BooleanField(_('is public')) + ip_address = models.IPAddressField(_('ip address')) + # TODO: Change this to is_removed, like Comment + approved = models.BooleanField(_('approved by staff')) + site = models.ForeignKey(Site) + class Meta: + verbose_name = _('free comment') + verbose_name_plural = _('free comments') + ordering = ('-submit_date',) + class Admin: + fields = ( + (None, {'fields': ('content_type', 'object_id', 'site')}), + ('Content', {'fields': ('person_name', 'comment')}), + ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}), + ) + list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object') + list_filter = ('submit_date',) + date_hierarchy = 'submit_date' + search_fields = ('comment', 'person_name') + + def __repr__(self): + return "%s: %s..." % (self.person_name, self.comment[:100]) + + def get_absolute_url(self): + return self.get_content_object().get_absolute_url() + "#c" + str(self.id) + + def get_content_object(self): + """ + Returns the object that this comment is a comment on. Returns None if + the object no longer exists. + """ + from django.core.exceptions import ObjectDoesNotExist + try: + return self.get_content_type().get_object_for_this_type(pk=self.object_id) + except ObjectDoesNotExist: + return None + + get_content_object.short_description = _('Content object') + +class KarmaScoreManager(models.Manager): + def vote(self, user_id, comment_id, score): + try: + karma = self.objects.get(comment__id__exact=comment_id, user__id__exact=user_id) + except self.model.DoesNotExist: + karma = self.model(None, user_id, comment_id, score, datetime.datetime.now()) + karma.save() + else: + karma.score = score + karma.scored_date = datetime.datetime.now() + karma.save() + + def get_pretty_score(self, score): + """ + Given a score between -1 and 1 (inclusive), returns the same score on a + scale between 1 and 10 (inclusive), as an integer. + """ + if score is None: + return DEFAULT_KARMA + return int(round((4.5 * score) + 5.5)) + +class KarmaScore(models.Model): + user = models.ForeignKey(User) + comment = models.ForeignKey(Comment) + score = models.SmallIntegerField(_('score'), db_index=True) + scored_date = models.DateTimeField(_('score date'), auto_now=True) + objects = KarmaScoreManager() + class Meta: + verbose_name = _('karma score') + verbose_name_plural = _('karma scores') + unique_together = (('user', 'comment'),) + + def __repr__(self): + return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.user} + +class UserFlagManager(models.Manager): + def flag(self, comment, user): + """ + Flags the given comment by the given user. If the comment has already + been flagged by the user, or it was a comment posted by the user, + nothing happens. + """ + if int(comment.user_id) == int(user.id): + return # A user can't flag his own comment. Fail silently. + try: + f = self.objects.get(user__id__exact=user.id, comment__id__exact=comment.id) + except self.model.DoesNotExist: + from django.core.mail import mail_managers + f = self.model(None, user.id, comment.id, None) + message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()} + mail_managers('Comment flagged', message, fail_silently=True) + f.save() + +class UserFlag(models.Model): + user = models.ForeignKey(User) + comment = models.ForeignKey(Comment) + flag_date = models.DateTimeField(_('flag date'), auto_now_add=True) + objects = UserFlagManager() + class Meta: + verbose_name = _('user flag') + verbose_name_plural = _('user flags') + unique_together = (('user', 'comment'),) + + def __repr__(self): + return _("Flag by %r") % self.user + +class ModeratorDeletion(models.Model): + user = models.ForeignKey(User, verbose_name='moderator') + comment = models.ForeignKey(Comment) + deletion_date = models.DateTimeField(_('deletion date'), auto_now_add=True) + class Meta: + verbose_name = _('moderator deletion') + verbose_name_plural = _('moderator deletions') + unique_together = (('user', 'comment'),) + + def __repr__(self): + return _("Moderator deletion by %r") % self.user diff --git a/django/contrib/comments/models/__init__.py b/django/contrib/comments/models/__init__.py deleted file mode 100644 index 2bf3cc96cc..0000000000 --- a/django/contrib/comments/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['comments'] diff --git a/django/contrib/comments/models/comments.py b/django/contrib/comments/models/comments.py deleted file mode 100644 index 3062af62de..0000000000 --- a/django/contrib/comments/models/comments.py +++ /dev/null @@ -1,287 +0,0 @@ -from django.core import meta -from django.models import auth, core -from django.utils.translation import gettext_lazy as _ - -class Comment(meta.Model): - user = meta.ForeignKey(auth.User, raw_id_admin=True) - content_type = meta.ForeignKey(core.ContentType) - object_id = meta.IntegerField(_('object ID')) - headline = meta.CharField(_('headline'), maxlength=255, blank=True) - comment = meta.TextField(_('comment'), maxlength=3000) - rating1 = meta.PositiveSmallIntegerField(_('rating #1'), blank=True, null=True) - rating2 = meta.PositiveSmallIntegerField(_('rating #2'), blank=True, null=True) - rating3 = meta.PositiveSmallIntegerField(_('rating #3'), blank=True, null=True) - rating4 = meta.PositiveSmallIntegerField(_('rating #4'), blank=True, null=True) - rating5 = meta.PositiveSmallIntegerField(_('rating #5'), blank=True, null=True) - rating6 = meta.PositiveSmallIntegerField(_('rating #6'), blank=True, null=True) - rating7 = meta.PositiveSmallIntegerField(_('rating #7'), blank=True, null=True) - rating8 = meta.PositiveSmallIntegerField(_('rating #8'), blank=True, null=True) - # This field designates whether to use this row's ratings in aggregate - # functions (summaries). We need this because people are allowed to post - # multiple reviews on the same thing, but the system will only use the - # latest one (with valid_rating=True) in tallying the reviews. - valid_rating = meta.BooleanField(_('is valid rating')) - submit_date = meta.DateTimeField(_('date/time submitted'), auto_now_add=True) - is_public = meta.BooleanField(_('is public')) - ip_address = meta.IPAddressField(_('IP address'), blank=True, null=True) - is_removed = meta.BooleanField(_('is removed'), help_text=_('Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.')) - site = meta.ForeignKey(core.Site) - class META: - db_table = 'comments' - verbose_name = _('Comment') - verbose_name_plural = _('Comments') - module_constants = { - # min. and max. allowed dimensions for photo resizing (in pixels) - 'MIN_PHOTO_DIMENSION': 5, - 'MAX_PHOTO_DIMENSION': 1000, - - # option codes for comment-form hidden fields - 'PHOTOS_REQUIRED': 'pr', - 'PHOTOS_OPTIONAL': 'pa', - 'RATINGS_REQUIRED': 'rr', - 'RATINGS_OPTIONAL': 'ra', - 'IS_PUBLIC': 'ip', - } - ordering = ('-submit_date',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('content_type', 'object_id', 'site')}), - ('Content', {'fields': ('user', 'headline', 'comment')}), - ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), - ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), - ), - list_display = ('user', 'submit_date', 'content_type', 'get_content_object'), - list_filter = ('submit_date',), - date_hierarchy = 'submit_date', - search_fields = ('comment', 'user__username'), - ) - - def __repr__(self): - return "%s: %s..." % (self.get_user().username, self.comment[:100]) - - def get_absolute_url(self): - return self.get_content_object().get_absolute_url() + "#c" + str(self.id) - - def get_crossdomain_url(self): - return "/r/%d/%d/" % (self.content_type_id, self.object_id) - - def get_flag_url(self): - return "/comments/flag/%s/" % self.id - - def get_deletion_url(self): - return "/comments/delete/%s/" % self.id - - def get_content_object(self): - """ - Returns the object that this comment is a comment on. Returns None if - the object no longer exists. - """ - from django.core.exceptions import ObjectDoesNotExist - try: - return self.get_content_type().get_object_for_this_type(pk=self.object_id) - except ObjectDoesNotExist: - return None - - get_content_object.short_description = _('Content object') - - def _fill_karma_cache(self): - "Helper function that populates good/bad karma caches" - good, bad = 0, 0 - for k in self.get_karmascore_list(): - if k.score == -1: - bad +=1 - elif k.score == 1: - good +=1 - self._karma_total_good, self._karma_total_bad = good, bad - - def get_good_karma_total(self): - if not hasattr(self, "_karma_total_good"): - self._fill_karma_cache() - return self._karma_total_good - - def get_bad_karma_total(self): - if not hasattr(self, "_karma_total_bad"): - self._fill_karma_cache() - return self._karma_total_bad - - def get_karma_total(self): - if not hasattr(self, "_karma_total_good") or not hasattr(self, "_karma_total_bad"): - self._fill_karma_cache() - return self._karma_total_good + self._karma_total_bad - - def get_as_text(self): - return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % \ - {'user': self.get_user().username, 'date': self.submit_date, - 'comment': self.comment, 'domain': self.get_site().domain, 'url': self.get_absolute_url()} - - def _module_get_security_hash(options, photo_options, rating_options, target): - """ - Returns the MD5 hash of the given options (a comma-separated string such as - 'pa,ra') and target (something like 'lcom.eventtimes:5157'). Used to - validate that submitted form options have not been tampered-with. - """ - from django.conf.settings import SECRET_KEY - import md5 - return md5.new(options + photo_options + rating_options + target + SECRET_KEY).hexdigest() - - def _module_get_rating_options(rating_string): - """ - Given a rating_string, this returns a tuple of (rating_range, options). - >>> s = "scale:1-10|First_category|Second_category" - >>> get_rating_options(s) - ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category']) - """ - rating_range, options = rating_string.split('|', 1) - rating_range = range(int(rating_range[6:].split('-')[0]), int(rating_range[6:].split('-')[1])+1) - choices = [c.replace('_', ' ') for c in options.split('|')] - return rating_range, choices - - def _module_get_list_with_karma(**kwargs): - """ - Returns a list of Comment objects matching the given lookup terms, with - _karma_total_good and _karma_total_bad filled. - """ - kwargs.setdefault('select', {}) - kwargs['select']['_karma_total_good'] = 'SELECT COUNT(*) FROM comments_karma WHERE comments_karma.comment_id=comments.id AND score=1' - kwargs['select']['_karma_total_bad'] = 'SELECT COUNT(*) FROM comments_karma WHERE comments_karma.comment_id=comments.id AND score=-1' - return get_list(**kwargs) - - def _module_user_is_moderator(user): - from django.conf.settings import COMMENTS_MODERATORS_GROUP - if user.is_superuser: - return True - for g in user.get_group_list(): - if g.id == COMMENTS_MODERATORS_GROUP: - return True - return False - -class FreeComment(meta.Model): - # A FreeComment is a comment by a non-registered user. - content_type = meta.ForeignKey(core.ContentType) - object_id = meta.IntegerField(_('object ID')) - comment = meta.TextField(_('comment'), maxlength=3000) - person_name = meta.CharField(_("person's name"), maxlength=50) - submit_date = meta.DateTimeField(_('date/time submitted'), auto_now_add=True) - is_public = meta.BooleanField(_('is public')) - ip_address = meta.IPAddressField(_('ip address')) - # TODO: Change this to is_removed, like Comment - approved = meta.BooleanField(_('approved by staff')) - site = meta.ForeignKey(core.Site) - class META: - db_table = 'comments_free' - verbose_name = _('Free comment') - verbose_name_plural = _('Free comments') - ordering = ('-submit_date',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('content_type', 'object_id', 'site')}), - ('Content', {'fields': ('person_name', 'comment')}), - ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}), - ), - list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object'), - list_filter = ('submit_date',), - date_hierarchy = 'submit_date', - search_fields = ('comment', 'person_name'), - ) - - def __repr__(self): - return "%s: %s..." % (self.person_name, self.comment[:100]) - - def get_absolute_url(self): - return self.get_content_object().get_absolute_url() + "#c" + str(self.id) - - def get_content_object(self): - """ - Returns the object that this comment is a comment on. Returns None if - the object no longer exists. - """ - from django.core.exceptions import ObjectDoesNotExist - try: - return self.get_content_type().get_object_for_this_type(pk=self.object_id) - except ObjectDoesNotExist: - return None - - get_content_object.short_description = _('Content object') - -class KarmaScore(meta.Model): - user = meta.ForeignKey(auth.User) - comment = meta.ForeignKey(Comment) - score = meta.SmallIntegerField(_('score'), db_index=True) - scored_date = meta.DateTimeField(_('score date'), auto_now=True) - class META: - module_name = 'karma' - verbose_name = _('Karma score') - verbose_name_plural = _('Karma scores') - unique_together = (('user', 'comment'),) - module_constants = { - # what users get if they don't have any karma - 'DEFAULT_KARMA': 5, - 'KARMA_NEEDED_BEFORE_DISPLAYED': 3, - } - - def __repr__(self): - return _("%(score)d rating by %(user)s") % {'score': self.score, 'user': self.get_user()} - - def _module_vote(user_id, comment_id, score): - try: - karma = get_object(comment__id__exact=comment_id, user__id__exact=user_id) - except KarmaScoreDoesNotExist: - karma = KarmaScore(None, user_id, comment_id, score, datetime.datetime.now()) - karma.save() - else: - karma.score = score - karma.scored_date = datetime.datetime.now() - karma.save() - - def _module_get_pretty_score(score): - """ - Given a score between -1 and 1 (inclusive), returns the same score on a - scale between 1 and 10 (inclusive), as an integer. - """ - if score is None: - return DEFAULT_KARMA - return int(round((4.5 * score) + 5.5)) - -class UserFlag(meta.Model): - user = meta.ForeignKey(auth.User) - comment = meta.ForeignKey(Comment) - flag_date = meta.DateTimeField(_('flag date'), auto_now_add=True) - class META: - db_table = 'comments_user_flags' - verbose_name = _('User flag') - verbose_name_plural = _('User flags') - unique_together = (('user', 'comment'),) - - def __repr__(self): - return _("Flag by %r") % self.get_user() - - def _module_flag(comment, user): - """ - Flags the given comment by the given user. If the comment has already - been flagged by the user, or it was a comment posted by the user, - nothing happens. - """ - if int(comment.user_id) == int(user.id): - return # A user can't flag his own comment. Fail silently. - try: - f = get_object(user__id__exact=user.id, comment__id__exact=comment.id) - except UserFlagDoesNotExist: - from django.core.mail import mail_managers - f = UserFlag(None, user.id, comment.id, None) - message = _('This comment was flagged by %(user)s:\n\n%(text)s') % {'user': user.username, 'text': comment.get_as_text()} - mail_managers('Comment flagged', message, fail_silently=True) - f.save() - -class ModeratorDeletion(meta.Model): - user = meta.ForeignKey(auth.User, verbose_name='moderator') - comment = meta.ForeignKey(Comment) - deletion_date = meta.DateTimeField(_('deletion date'), auto_now_add=True) - class META: - db_table = 'comments_moderator_deletions' - verbose_name = _('Moderator deletion') - verbose_name_plural = _('Moderator deletions') - unique_together = (('user', 'comment'),) - - def __repr__(self): - return _("Moderator deletion by %r") % self.get_user() - diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index d71565321f..a3893fdf61 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -1,16 +1,16 @@ -"Custom template tags for user comments" - -from django.core import template -from django.core.template import loader +from django.contrib.comments.models import Comment, FreeComment +from django.contrib.comments.models import PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC +from django.contrib.comments.models import MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION +from django import template +from django.template import loader from django.core.exceptions import ObjectDoesNotExist -from django.models.comments import comments, freecomments -from django.models.core import contenttypes +from django.contrib.contenttypes.models import ContentType import re register = template.Library() -COMMENT_FORM = 'comments/form' -FREE_COMMENT_FORM = 'comments/freeform' +COMMENT_FORM = 'comments/form.html' +FREE_COMMENT_FORM = 'comments/freeform.html' class CommentFormNode(template.Node): def __init__(self, content_type, obj_id_lookup_var, obj_id, free, @@ -46,24 +46,24 @@ class CommentFormNode(template.Node): context['display_form'] = True context['target'] = '%s:%s' % (self.content_type.id, self.obj_id) options = [] - for var, abbr in (('photos_required', comments.PHOTOS_REQUIRED), - ('photos_optional', comments.PHOTOS_OPTIONAL), - ('ratings_required', comments.RATINGS_REQUIRED), - ('ratings_optional', comments.RATINGS_OPTIONAL), - ('is_public', comments.IS_PUBLIC)): + for var, abbr in (('photos_required', PHOTOS_REQUIRED), + ('photos_optional', PHOTOS_OPTIONAL), + ('ratings_required', RATINGS_REQUIRED), + ('ratings_optional', RATINGS_OPTIONAL), + ('is_public', IS_PUBLIC)): context[var] = getattr(self, var) if getattr(self, var): options.append(abbr) context['options'] = ','.join(options) if self.free: - context['hash'] = comments.get_security_hash(context['options'], '', '', context['target']) + context['hash'] = Comment.objects.get_security_hash(context['options'], '', '', context['target']) default_form = loader.get_template(FREE_COMMENT_FORM) else: context['photo_options'] = self.photo_options context['rating_options'] = normalize_newlines(base64.encodestring(self.rating_options).strip()) if self.rating_options: - context['rating_range'], context['rating_choices'] = comments.get_rating_options(self.rating_options) - context['hash'] = comments.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target']) + context['rating_range'], context['rating_choices'] = Comment.objects.get_rating_options(self.rating_options) + context['hash'] = Comment.objects.get_security_hash(context['options'], context['photo_options'], context['rating_options'], context['target']) default_form = loader.get_template(COMMENT_FORM) output = default_form.render(context) context.pop() @@ -76,13 +76,13 @@ class CommentCountNode(template.Node): self.var_name, self.free = var_name, free def render(self, context): - from django.conf.settings import SITE_ID - get_count_function = self.free and freecomments.get_count or comments.get_count + from django.conf import settings + manager = self.free and FreeComment.objects or Comment.objects if self.context_var_name is not None: self.obj_id = template.resolve_variable(self.context_var_name, context) - comment_count = get_count_function(object_id__exact=self.obj_id, - content_type__package__label__exact=self.package, - content_type__python_module_name__exact=self.module, site__id__exact=SITE_ID) + comment_count = manager.filter(object_id__exact=self.obj_id, + content_type__app_label__exact=self.package, + content_type__model__exact=self.module, site__id__exact=settings.SITE_ID).count() context[self.var_name] = comment_count return '' @@ -95,8 +95,8 @@ class CommentListNode(template.Node): self.extra_kwargs = extra_kwargs or {} def render(self, context): - from django.conf.settings import COMMENTS_BANNED_USERS_GROUP, SITE_ID - get_list_function = self.free and freecomments.get_list or comments.get_list_with_karma + from django.conf import settings + get_list_function = self.free and FreeComment.objects.filter or Comment.objects.get_list_with_karma if self.context_var_name is not None: try: self.obj_id = template.resolve_variable(self.context_var_name, context) @@ -104,26 +104,24 @@ class CommentListNode(template.Node): return '' kwargs = { 'object_id__exact': self.obj_id, - 'content_type__package__label__exact': self.package, - 'content_type__python_module_name__exact': self.module, - 'site__id__exact': SITE_ID, - 'select_related': True, - 'order_by': (self.ordering + 'submit_date',), + 'content_type__app_label__exact': self.package, + 'content_type__model__exact': self.module, + 'site__id__exact': settings.SITE_ID, } kwargs.update(self.extra_kwargs) - if not self.free and COMMENTS_BANNED_USERS_GROUP: - kwargs['select'] = {'is_hidden': 'user_id IN (SELECT user_id FROM auth_users_groups WHERE group_id = %s)' % COMMENTS_BANNED_USERS_GROUP} - comment_list = get_list_function(**kwargs) + if not self.free and settings.COMMENTS_BANNED_USERS_GROUP: + kwargs['select'] = {'is_hidden': 'user_id IN (SELECT user_id FROM auth_user_groups WHERE group_id = %s)' % settings.COMMENTS_BANNED_USERS_GROUP} + comment_list = get_list_function(**kwargs).order_by(self.ordering + 'submit_date').select_related() if not self.free: if context.has_key('user') and not context['user'].is_anonymous(): user_id = context['user'].id - context['user_can_moderate_comments'] = comments.user_is_moderator(context['user']) + context['user_can_moderate_comments'] = Comment.objects.user_is_moderator(context['user']) else: user_id = None context['user_can_moderate_comments'] = False # Only display comments by banned users to those users themselves. - if COMMENTS_BANNED_USERS_GROUP: + if settings.COMMENTS_BANNED_USERS_GROUP: comment_list = [c for c in comment_list if not c.is_hidden or (user_id == c.user_id)] context[self.var_name] = comment_list @@ -157,8 +155,8 @@ class DoCommentForm: except ValueError: # unpack list of wrong size raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] try: - content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module) - except contenttypes.ContentTypeDoesNotExist: + content_type = ContentType.objects.get(app_label__exact=package, model__exact=module) + except ContentType.DoesNotExist: raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) obj_id_lookup_var, obj_id = None, None if tokens[3].isdigit(): @@ -183,8 +181,8 @@ class DoCommentForm: if not opt.isalnum(): raise template.TemplateSyntaxError, "Invalid photo directory name in %r tag: '%s'" % (tokens[0], opt) for opt in option_list[1::3] + option_list[2::3]: - if not opt.isdigit() or not (comments.MIN_PHOTO_DIMENSION <= int(opt) <= comments.MAX_PHOTO_DIMENSION): - raise template.TemplateSyntaxError, "Invalid photo dimension in %r tag: '%s'. Only values between %s and %s are allowed." % (tokens[0], opt, comments.MIN_PHOTO_DIMENSION, comments.MAX_PHOTO_DIMENSION) + if not opt.isdigit() or not (MIN_PHOTO_DIMENSION <= int(opt) <= MAX_PHOTO_DIMENSION): + raise template.TemplateSyntaxError, "Invalid photo dimension in %r tag: '%s'. Only values between %s and %s are allowed." % (tokens[0], opt, MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION) # VALIDATION ENDS ######################################### kwargs[option] = True kwargs['photo_options'] = args @@ -237,8 +235,8 @@ class DoCommentCount: except ValueError: # unpack list of wrong size raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] try: - content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module) - except contenttypes.ContentTypeDoesNotExist: + content_type = ContentType.objects.get(app_label__exact=package, model__exact=module) + except ContentType.DoesNotExist: raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) var_name, obj_id = None, None if tokens[3].isdigit(): @@ -292,8 +290,8 @@ class DoGetCommentList: except ValueError: # unpack list of wrong size raise template.TemplateSyntaxError, "Third argument in %r tag must be in the format 'package.module'" % tokens[0] try: - content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module) - except contenttypes.ContentTypeDoesNotExist: + content_type = ContentType.objects.get(app_label__exact=package,model__exact=module) + except ContentType.DoesNotExist: raise template.TemplateSyntaxError, "%r tag has invalid content-type '%s.%s'" % (tokens[0], package, module) var_name, obj_id = None, None if tokens[3].isdigit(): diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 5918db7dc8..ecacc1f1c5 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -1,14 +1,17 @@ -from django.core import formfields, validators +from django.core import validators +from django import forms from django.core.mail import mail_admins, mail_managers -from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.extensions import DjangoContext, render_to_response -from django.models.auth import users -from django.models.comments import comments, freecomments -from django.models.core import contenttypes -from django.parts.auth.formfields import AuthenticationForm -from django.utils.httpwrappers import HttpResponseRedirect +from django.http import Http404 +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.contrib.auth.models import SESSION_KEY +from django.contrib.comments.models import Comment, FreeComment, PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.forms import AuthenticationForm +from django.http import HttpResponseRedirect from django.utils.text import normalize_newlines -from django.conf.settings import BANNED_IPS, COMMENTS_ALLOW_PROFANITIES, COMMENTS_SKETCHY_USERS_GROUP, COMMENTS_FIRST_FEW, SITE_ID +from django.conf import settings from django.utils.translation import ngettext import base64, datetime @@ -26,37 +29,37 @@ class PublicCommentManipulator(AuthenticationForm): else: return [] self.fields.extend([ - formfields.LargeTextField(field_name="comment", maxlength=3000, is_required=True, + forms.LargeTextField(field_name="comment", maxlength=3000, is_required=True, validator_list=[self.hasNoProfanities]), - formfields.RadioSelectField(field_name="rating1", choices=choices, + forms.RadioSelectField(field_name="rating1", choices=choices, is_required=ratings_required and num_rating_choices > 0, validator_list=get_validator_list(1), ), - formfields.RadioSelectField(field_name="rating2", choices=choices, + forms.RadioSelectField(field_name="rating2", choices=choices, is_required=ratings_required and num_rating_choices > 1, validator_list=get_validator_list(2), ), - formfields.RadioSelectField(field_name="rating3", choices=choices, + forms.RadioSelectField(field_name="rating3", choices=choices, is_required=ratings_required and num_rating_choices > 2, validator_list=get_validator_list(3), ), - formfields.RadioSelectField(field_name="rating4", choices=choices, + forms.RadioSelectField(field_name="rating4", choices=choices, is_required=ratings_required and num_rating_choices > 3, validator_list=get_validator_list(4), ), - formfields.RadioSelectField(field_name="rating5", choices=choices, + forms.RadioSelectField(field_name="rating5", choices=choices, is_required=ratings_required and num_rating_choices > 4, validator_list=get_validator_list(5), ), - formfields.RadioSelectField(field_name="rating6", choices=choices, + forms.RadioSelectField(field_name="rating6", choices=choices, is_required=ratings_required and num_rating_choices > 5, validator_list=get_validator_list(6), ), - formfields.RadioSelectField(field_name="rating7", choices=choices, + forms.RadioSelectField(field_name="rating7", choices=choices, is_required=ratings_required and num_rating_choices > 6, validator_list=get_validator_list(7), ), - formfields.RadioSelectField(field_name="rating8", choices=choices, + forms.RadioSelectField(field_name="rating8", choices=choices, is_required=ratings_required and num_rating_choices > 7, validator_list=get_validator_list(8), ), @@ -69,25 +72,25 @@ class PublicCommentManipulator(AuthenticationForm): self.user_cache = user def hasNoProfanities(self, field_data, all_data): - if COMMENTS_ALLOW_PROFANITIES: + if settings.COMMENTS_ALLOW_PROFANITIES: return return validators.hasNoProfanities(field_data, all_data) def get_comment(self, new_data): "Helper function" - return comments.Comment(None, self.get_user_id(), new_data["content_type_id"], + return Comment(None, self.get_user_id(), new_data["content_type_id"], new_data["object_id"], new_data.get("headline", "").strip(), new_data["comment"].strip(), new_data.get("rating1", None), new_data.get("rating2", None), new_data.get("rating3", None), new_data.get("rating4", None), new_data.get("rating5", None), new_data.get("rating6", None), new_data.get("rating7", None), new_data.get("rating8", None), new_data.get("rating1", None) is not None, - datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, SITE_ID) + datetime.datetime.now(), new_data["is_public"], new_data["ip_address"], False, settings.SITE_ID) def save(self, new_data): today = datetime.date.today() c = self.get_comment(new_data) - for old in comments.get_list(content_type__id__exact=new_data["content_type_id"], + for old in Comment.objects.filter(content_type__id__exact=new_data["content_type_id"], object_id__exact=new_data["object_id"], user__id__exact=self.get_user_id()): # Check that this comment isn't duplicate. (Sometimes people post # comments twice by mistake.) If it is, fail silently by pretending @@ -105,37 +108,37 @@ class PublicCommentManipulator(AuthenticationForm): c.save() # If the commentor has posted fewer than COMMENTS_FIRST_FEW comments, # send the comment to the managers. - if self.user_cache.get_comments_comment_count() <= COMMENTS_FIRST_FEW: + if self.user_cache.comment_set.count() <= settings.COMMENTS_FIRST_FEW: message = ngettext('This comment was posted by a user who has posted fewer than %(count)s comment:\n\n%(text)s', 'This comment was posted by a user who has posted fewer than %(count)s comments:\n\n%(text)s') % \ - {'count': COMMENTS_FIRST_FEW, 'text': c.get_as_text()} + {'count': settings.COMMENTS_FIRST_FEW, 'text': c.get_as_text()} mail_managers("Comment posted by rookie user", message) - if COMMENTS_SKETCHY_USERS_GROUP and COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.get_group_list()]: + if settings.COMMENTS_SKETCHY_USERS_GROUP and settings.COMMENTS_SKETCHY_USERS_GROUP in [g.id for g in self.user_cache.get_group_list()]: message = _('This comment was posted by a sketchy user:\n\n%(text)s') % {'text': c.get_as_text()} mail_managers("Comment posted by sketchy user (%s)" % self.user_cache.username, c.get_as_text()) return c -class PublicFreeCommentManipulator(formfields.Manipulator): +class PublicFreeCommentManipulator(forms.Manipulator): "Manipulator that handles public free (unregistered) comments" def __init__(self): self.fields = ( - formfields.TextField(field_name="person_name", maxlength=50, is_required=True, + forms.TextField(field_name="person_name", maxlength=50, is_required=True, validator_list=[self.hasNoProfanities]), - formfields.LargeTextField(field_name="comment", maxlength=3000, is_required=True, + forms.LargeTextField(field_name="comment", maxlength=3000, is_required=True, validator_list=[self.hasNoProfanities]), ) def hasNoProfanities(self, field_data, all_data): - if COMMENTS_ALLOW_PROFANITIES: + if settings.COMMENTS_ALLOW_PROFANITIES: return return validators.hasNoProfanities(field_data, all_data) def get_comment(self, new_data): "Helper function" - return freecomments.FreeComment(None, new_data["content_type_id"], + return FreeComment(None, new_data["content_type_id"], new_data["object_id"], new_data["comment"].strip(), new_data["person_name"].strip(), datetime.datetime.now(), new_data["is_public"], - new_data["ip_address"], False, SITE_ID) + new_data["ip_address"], False, settings.SITE_ID) def save(self, new_data): today = datetime.date.today() @@ -143,7 +146,7 @@ class PublicFreeCommentManipulator(formfields.Manipulator): # Check that this comment isn't duplicate. (Sometimes people post # comments twice by mistake.) If it is, fail silently by pretending # the comment was posted successfully. - for old_comment in freecomments.get_list(content_type__id__exact=new_data["content_type_id"], + for old_comment in FreeComment.objects.filter(content_type__id__exact=new_data["content_type_id"], object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"], submit_date__year=today.year, submit_date__month=today.month, submit_date__day=today.day): @@ -190,16 +193,16 @@ def post_comment(request): raise Http404, _("One or more of the required fields wasn't submitted") photo_options = request.POST.get('photo_options', '') rating_options = normalize_newlines(request.POST.get('rating_options', '')) - if comments.get_security_hash(options, photo_options, rating_options, target) != security_hash: + if Comment.objects.get_security_hash(options, photo_options, rating_options, target) != security_hash: raise Http404, _("Somebody tampered with the comment form (security violation)") # Now we can be assured the data is valid. if rating_options: - rating_range, rating_choices = comments.get_rating_options(base64.decodestring(rating_options)) + rating_range, rating_choices = Comment.objects.get_rating_options(base64.decodestring(rating_options)) else: rating_range, rating_choices = [], [] content_type_id, object_id = target.split(':') # target is something like '52:5157' try: - obj = contenttypes.get_object(pk=content_type_id).get_object_for_this_type(pk=object_id) + obj = ContentType.objects.get(pk=content_type_id).get_object_for_this_type(pk=object_id) except ObjectDoesNotExist: raise Http404, _("The comment form had an invalid 'target' parameter -- the object ID was invalid") option_list = options.split(',') # options is something like 'pa,ra' @@ -207,20 +210,20 @@ def post_comment(request): new_data['content_type_id'] = content_type_id new_data['object_id'] = object_id new_data['ip_address'] = request.META.get('REMOTE_ADDR') - new_data['is_public'] = comments.IS_PUBLIC in option_list + new_data['is_public'] = IS_PUBLIC in option_list manipulator = PublicCommentManipulator(request.user, - ratings_required=comments.RATINGS_REQUIRED in option_list, + ratings_required=RATINGS_REQUIRED in option_list, ratings_range=rating_range, num_rating_choices=len(rating_choices)) errors = manipulator.get_validation_errors(new_data) # If user gave correct username/password and wasn't already logged in, log them in # so they don't have to enter a username/password again. if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']): - request.session[users.SESSION_KEY] = manipulator.get_user_id() + request.session[SESSION_KEY] = manipulator.get_user_id() if errors or request.POST.has_key('preview'): - class CommentFormWrapper(formfields.FormWrapper): + class CommentFormWrapper(forms.FormWrapper): def __init__(self, manipulator, new_data, errors, rating_choices): - formfields.FormWrapper.__init__(self, manipulator, new_data, errors) + forms.FormWrapper.__init__(self, manipulator, new_data, errors) self.rating_choices = rating_choices def ratings(self): field_list = [self['rating%d' % (i+1)] for i in range(len(rating_choices))] @@ -229,22 +232,22 @@ def post_comment(request): return field_list comment = errors and '' or manipulator.get_comment(new_data) comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices) - return render_to_response('comments/preview', { + return render_to_response('comments/preview.html', { 'comment': comment, 'comment_form': comment_form, 'options': options, 'target': target, 'hash': security_hash, 'rating_options': rating_options, - 'ratings_optional': comments.RATINGS_OPTIONAL in option_list, - 'ratings_required': comments.RATINGS_REQUIRED in option_list, + 'ratings_optional': RATINGS_OPTIONAL in option_list, + 'ratings_required': RATINGS_REQUIRED in option_list, 'rating_range': rating_range, 'rating_choices': rating_choices, - }, context_instance=DjangoContext(request)) + }, context_instance=RequestContext(request)) elif request.POST.has_key('post'): # If the IP is banned, mail the admins, do NOT save the comment, and # serve up the "Thanks for posting" page as if the comment WAS posted. - if request.META['REMOTE_ADDR'] in BANNED_IPS: + if request.META['REMOTE_ADDR'] in settings.BANNED_IPS: mail_admins("Banned IP attempted to post comment", str(request.POST) + "\n\n" + str(request.META)) else: manipulator.do_html2python(new_data) @@ -279,10 +282,10 @@ def post_free_comment(request): options, target, security_hash = request.POST['options'], request.POST['target'], request.POST['gonzo'] except KeyError: raise Http404, _("One or more of the required fields wasn't submitted") - if comments.get_security_hash(options, '', '', target) != security_hash: + if Comment.objects.get_security_hash(options, '', '', target) != security_hash: raise Http404, _("Somebody tampered with the comment form (security violation)") content_type_id, object_id = target.split(':') # target is something like '52:5157' - content_type = contenttypes.get_object(pk=content_type_id) + content_type = ContentType.objects.get(pk=content_type_id) try: obj = content_type.get_object_for_this_type(pk=object_id) except ObjectDoesNotExist: @@ -292,22 +295,22 @@ def post_free_comment(request): new_data['content_type_id'] = content_type_id new_data['object_id'] = object_id new_data['ip_address'] = request.META['REMOTE_ADDR'] - new_data['is_public'] = comments.IS_PUBLIC in option_list + new_data['is_public'] = IS_PUBLIC in option_list manipulator = PublicFreeCommentManipulator() errors = manipulator.get_validation_errors(new_data) if errors or request.POST.has_key('preview'): comment = errors and '' or manipulator.get_comment(new_data) - return render_to_response('comments/free_preview', { + return render_to_response('comments/free_preview.html', { 'comment': comment, - 'comment_form': formfields.FormWrapper(manipulator, new_data, errors), + 'comment_form': forms.FormWrapper(manipulator, new_data, errors), 'options': options, 'target': target, 'hash': security_hash, - }, context_instance=DjangoContext(request)) + }, context_instance=RequestContext(request)) elif request.POST.has_key('post'): # If the IP is banned, mail the admins, do NOT save the comment, and # serve up the "Thanks for posting" page as if the comment WAS posted. - if request.META['REMOTE_ADDR'] in BANNED_IPS: + if request.META['REMOTE_ADDR'] in settings.BANNED_IPS: from django.core.mail import mail_admins mail_admins("Practical joker", str(request.POST) + "\n\n" + str(request.META)) else: @@ -330,8 +333,8 @@ def comment_was_posted(request): if request.GET.has_key('c'): content_type_id, object_id = request.GET['c'].split(':') try: - content_type = contenttypes.get_object(pk=content_type_id) + content_type = ContentType.objects.get(pk=content_type_id) obj = content_type.get_object_for_this_type(pk=object_id) except ObjectDoesNotExist: pass - return render_to_response('comments/posted', {'object': obj}, context_instance=DjangoContext(request)) + return render_to_response('comments/posted.html', {'object': obj}, context_instance=RequestContext(request)) diff --git a/django/contrib/comments/views/karma.py b/django/contrib/comments/views/karma.py index 9db68d69f1..becb02e128 100644 --- a/django/contrib/comments/views/karma.py +++ b/django/contrib/comments/views/karma.py @@ -1,6 +1,7 @@ -from django.core.exceptions import Http404 -from django.core.extensions import DjangoContext, render_to_response -from django.models.comments import comments, karma +from django.http import Http404 +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.contrib.comments.models import Comment, KarmaScore def vote(request, comment_id, vote): """ @@ -17,12 +18,12 @@ def vote(request, comment_id, vote): if request.user.is_anonymous(): raise Http404, _("Anonymous users cannot vote") try: - comment = comments.get_object(pk=comment_id) - except comments.CommentDoesNotExist: + comment = Comment.objects.get(pk=comment_id) + except Comment.DoesNotExist: raise Http404, _("Invalid comment ID") - if comment.user_id == request.user.id: + if comment.user.id == request.user.id: raise Http404, _("No voting for yourself") - karma.vote(request.user.id, comment_id, rating) + KarmaScore.objects.vote(request.user.id, comment_id, rating) # Reload comment to ensure we have up to date karma count - comment = comments.get_object(pk=comment_id) - return render_to_response('comments/karma_vote_accepted', {'comment': comment}, context_instance=DjangoContext(request)) + comment = Comment.objects.get(pk=comment_id) + return render_to_response('comments/karma_vote_accepted.html', {'comment': comment}, context_instance=RequestContext(request)) diff --git a/django/contrib/comments/views/userflags.py b/django/contrib/comments/views/userflags.py index 3bcc5b2122..76f14ef19c 100644 --- a/django/contrib/comments/views/userflags.py +++ b/django/contrib/comments/views/userflags.py @@ -1,9 +1,10 @@ -from django.core.extensions import DjangoContext, render_to_response -from django.core.exceptions import Http404 -from django.models.comments import comments, moderatordeletions, userflags -from django.views.decorators.auth import login_required -from django.utils.httpwrappers import HttpResponseRedirect -from django.conf.settings import SITE_ID +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.http import Http404 +from django.contrib.comments.models import Comment, ModeratorDeletion, UserFlag +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseRedirect +from django.conf import settings def flag(request, comment_id): """ @@ -14,22 +15,16 @@ def flag(request, comment_id): comment the flagged `comments.comments` object """ - try: - comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) - except comments.CommentDoesNotExist: - raise Http404 + comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID) if request.POST: - userflags.flag(comment, request.user) + UserFlag.objects.flag(comment, request.user) return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response('comments/flag_verify', {'comment': comment}, context_instance=DjangoContext(request)) + return render_to_response('comments/flag_verify.html', {'comment': comment}, context_instance=RequestContext(request)) flag = login_required(flag) def flag_done(request, comment_id): - try: - comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) - except comments.CommentDoesNotExist: - raise Http404 - return render_to_response('comments/flag_done', {'comment': comment}, context_instance=DjangoContext(request)) + comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID) + return render_to_response('comments/flag_done.html', {'comment': comment}, context_instance=RequestContext(request)) def delete(request, comment_id): """ @@ -40,26 +35,20 @@ def delete(request, comment_id): comment the flagged `comments.comments` object """ - try: - comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) - except comments.CommentDoesNotExist: - raise Http404 - if not comments.user_is_moderator(request.user): + comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID) + if not Comment.objects.user_is_moderator(request.user): raise Http404 if request.POST: # If the comment has already been removed, silently fail. if not comment.is_removed: comment.is_removed = True comment.save() - m = moderatordeletions.ModeratorDeletion(None, request.user.id, comment.id, None) + m = ModeratorDeletion(None, request.user.id, comment.id, None) m.save() return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response('comments/delete_verify', {'comment': comment}, context_instance=DjangoContext(request)) + return render_to_response('comments/delete_verify.html', {'comment': comment}, context_instance=RequestContext(request)) delete = login_required(delete) def delete_done(request, comment_id): - try: - comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) - except comments.CommentDoesNotExist: - raise Http404 - return render_to_response('comments/delete_done', {'comment': comment}, context_instance=DjangoContext(request)) + comment = get_object_or_404(Comment,pk=comment_id, site__id__exact=settings.SITE_ID) + return render_to_response('comments/delete_done.html', {'comment': comment}, context_instance=RequestContext(request)) diff --git a/django/contrib/admin/urls/__init__.py b/django/contrib/contenttypes/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/contrib/admin/urls/__init__.py +++ b/django/contrib/contenttypes/__init__.py diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py new file mode 100644 index 0000000000..eb6029e7d4 --- /dev/null +++ b/django/contrib/contenttypes/models.py @@ -0,0 +1,49 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +class ContentTypeManager(models.Manager): + def get_for_model(self, model): + """ + Returns the ContentType object for the given model, creating the + ContentType if necessary. + """ + opts = model._meta + try: + return self.model._default_manager.get(app_label=opts.app_label, + model=opts.object_name.lower()) + except self.model.DoesNotExist: + # The str() is needed around opts.verbose_name because it's a + # django.utils.functional.__proxy__ object. + ct = self.model(name=str(opts.verbose_name), + app_label=opts.app_label, model=opts.object_name.lower()) + ct.save() + return ct + +class ContentType(models.Model): + name = models.CharField(maxlength=100) + app_label = models.CharField(maxlength=100) + model = models.CharField(_('python model class name'), maxlength=100) + objects = ContentTypeManager() + class Meta: + verbose_name = _('content type') + verbose_name_plural = _('content types') + db_table = 'django_content_type' + ordering = ('name',) + unique_together = (('app_label', 'model'),) + + def __repr__(self): + return self.name + + def model_class(self): + "Returns the Python model class for this type of content." + from django.db import models + return models.get_model(self.app_label, self.model) + + def get_object_for_this_type(self, **kwargs): + """ + Returns an object of this type for the keyword arguments given. + Basically, this is a proxy around this object_type's get_object() model + method. The ObjectNotExist exception, if thrown, will not be caught, + so code that calls this method should catch it. + """ + return self.model_class()._default_manager.get(**kwargs) diff --git a/django/contrib/flatpages/middleware.py b/django/contrib/flatpages/middleware.py index c6f286563d..074e4ea735 100644 --- a/django/contrib/flatpages/middleware.py +++ b/django/contrib/flatpages/middleware.py @@ -1,6 +1,6 @@ from django.contrib.flatpages.views import flatpage -from django.core.extensions import Http404 -from django.conf.settings import DEBUG +from django.http import Http404 +from django.conf import settings class FlatpageFallbackMiddleware: def process_response(self, request, response): @@ -13,6 +13,6 @@ class FlatpageFallbackMiddleware: except Http404: return response except: - if DEBUG: + if settings.DEBUG: raise return response diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py new file mode 100644 index 0000000000..a60a536923 --- /dev/null +++ b/django/contrib/flatpages/models.py @@ -0,0 +1,33 @@ +from django.core import validators +from django.db import models +from django.contrib.sites.models import Site +from django.utils.translation import gettext_lazy as _ + +class FlatPage(models.Model): + url = models.CharField(_('URL'), maxlength=100, validator_list=[validators.isAlphaNumericURL], + help_text=_("Example: '/about/contact/'. Make sure to have leading and trailing slashes.")) + title = models.CharField(_('title'), maxlength=200) + content = models.TextField(_('content')) + enable_comments = models.BooleanField(_('enable comments')) + template_name = models.CharField(_('template name'), maxlength=70, blank=True, + help_text=_("Example: 'flatpages/contact_page'. If this isn't provided, the system will use 'flatpages/default'.")) + registration_required = models.BooleanField(_('registration required'), help_text=_("If this is checked, only logged-in users will be able to view the page.")) + sites = models.ManyToManyField(Site) + class Meta: + db_table = 'django_flatpage' + verbose_name = _('flat page') + verbose_name_plural = _('flat pages') + ordering = ('url',) + class Admin: + fields = ( + (None, {'fields': ('url', 'title', 'content', 'sites')}), + ('Advanced options', {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}), + ) + list_filter = ('sites',) + search_fields = ('url', 'title') + + def __repr__(self): + return "%s -- %s" % (self.url, self.title) + + def get_absolute_url(self): + return self.url diff --git a/django/contrib/flatpages/models/__init__.py b/django/contrib/flatpages/models/__init__.py deleted file mode 100644 index bb46349b5a..0000000000 --- a/django/contrib/flatpages/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['flatpages'] diff --git a/django/contrib/flatpages/models/flatpages.py b/django/contrib/flatpages/models/flatpages.py deleted file mode 100644 index cfb2741d34..0000000000 --- a/django/contrib/flatpages/models/flatpages.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.core import meta, validators -from django.models.core import Site -from django.utils.translation import gettext_lazy as _ - -class FlatPage(meta.Model): - url = meta.CharField(_('URL'), maxlength=100, validator_list=[validators.isAlphaNumericURL], - help_text=_("Example: '/about/contact/'. Make sure to have leading and trailing slashes.")) - title = meta.CharField(_('title'), maxlength=200) - content = meta.TextField(_('content')) - enable_comments = meta.BooleanField(_('enable comments')) - template_name = meta.CharField(_('template name'), maxlength=70, blank=True, - help_text=_("Example: 'flatpages/contact_page'. If this isn't provided, the system will use 'flatpages/default'.")) - registration_required = meta.BooleanField(_('registration required'), help_text=_("If this is checked, only logged-in users will be able to view the page.")) - sites = meta.ManyToManyField(Site) - class META: - db_table = 'django_flatpages' - verbose_name = _('flat page') - verbose_name_plural = _('flat pages') - ordering = ('url',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('url', 'title', 'content', 'sites')}), - ('Advanced options', {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}), - ), - list_filter = ('sites',), - search_fields = ('url', 'title'), - ) - - def __repr__(self): - return "%s -- %s" % (self.url, self.title) - - def get_absolute_url(self): - return self.url diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 48cd4526ab..1441f6f3a3 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,10 +1,10 @@ -from django.core import template_loader -from django.core.extensions import get_object_or_404, DjangoContext -from django.models.flatpages import flatpages -from django.utils.httpwrappers import HttpResponse -from django.conf.settings import SITE_ID +from django.contrib.flatpages.models import FlatPage +from django.template import loader, RequestContext +from django.shortcuts import get_object_or_404 +from django.http import HttpResponse +from django.conf import settings -DEFAULT_TEMPLATE = 'flatpages/default' +DEFAULT_TEMPLATE = 'flatpages/default.html' def flatpage(request, url): """ @@ -12,24 +12,24 @@ def flatpage(request, url): Models: `flatpages.flatpages` Templates: Uses the template defined by the ``template_name`` field, - or `flatpages/default` if template_name is not defined. + or `flatpages/default.html` if template_name is not defined. Context: flatpage `flatpages.flatpages` object """ if not url.startswith('/'): url = "/" + url - f = get_object_or_404(flatpages, url__exact=url, sites__id__exact=SITE_ID) + f = get_object_or_404(FlatPage, url__exact=url, sites__id__exact=settings.SITE_ID) # If registration is required for accessing this page, and the user isn't # logged in, redirect to the login page. if f.registration_required and request.user.is_anonymous(): - from django.views.auth.login import redirect_to_login + from django.contrib.auth.views import redirect_to_login return redirect_to_login(request.path) if f.template_name: - t = template_loader.select_template((f.template_name, DEFAULT_TEMPLATE)) + t = loader.select_template((f.template_name, DEFAULT_TEMPLATE)) else: - t = template_loader.get_template(DEFAULT_TEMPLATE) - c = DjangoContext(request, { + t = loader.get_template(DEFAULT_TEMPLATE) + c = RequestContext(request, { 'flatpage': f, }) return HttpResponse(t.render(c)) diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 5124889b89..dc8a9da031 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -14,7 +14,8 @@ In each case, if the required library is not installed, the filter will silently fail and return the un-marked-up text. """ -from django.core import template +from django import template +from django.conf import settings register = template.Library() @@ -22,6 +23,8 @@ def textile(value): try: import textile except ImportError: + if settings.DEBUG: + raise template.TemplateSyntaxError, "Error in {% textile %} filter: The Python textile library isn't installed." return value else: return textile.textile(value) @@ -30,6 +33,8 @@ def markdown(value): try: import markdown except ImportError: + if settings.DEBUG: + raise template.TemplateSyntaxError, "Error in {% markdown %} filter: The Python markdown library isn't installed." return value else: return markdown.markdown(value) @@ -38,6 +43,8 @@ def restructuredtext(value): try: from docutils.core import publish_parts except ImportError: + if settings.DEBUG: + raise template.TemplateSyntaxError, "Error in {% restructuredtext %} filter: The Python docutils library isn't installed." return value else: parts = publish_parts(source=value, writer_name="html4css1") diff --git a/django/contrib/redirects/middleware.py b/django/contrib/redirects/middleware.py index c005eba9ac..1960bffa12 100644 --- a/django/contrib/redirects/middleware.py +++ b/django/contrib/redirects/middleware.py @@ -1,6 +1,6 @@ -from django.models.redirects import redirects -from django.utils import httpwrappers -from django.conf.settings import APPEND_SLASH, SITE_ID +from django.contrib.redirects.models import Redirect +from django import http +from django.conf import settings class RedirectFallbackMiddleware: def process_response(self, request, response): @@ -8,20 +8,20 @@ class RedirectFallbackMiddleware: return response # No need to check for a redirect for non-404 responses. path = request.get_full_path() try: - r = redirects.get_object(site__id__exact=SITE_ID, old_path__exact=path) - except redirects.RedirectDoesNotExist: + r = Redirect.objects.get(site__id__exact=settings.SITE_ID, old_path=path) + except Redirect.DoesNotExist: r = None - if r is None and APPEND_SLASH: + if r is None and settings.APPEND_SLASH: # Try removing the trailing slash. try: - r = redirects.get_object(site__id__exact=SITE_ID, - old_path__exact=path[:path.rfind('/')]+path[path.rfind('/')+1:]) - except redirects.RedirectDoesNotExist: + r = Redirect.objects.get(site__id__exact=settings.SITE_ID, + old_path=path[:path.rfind('/')]+path[path.rfind('/')+1:]) + except Redirect.DoesNotExist: pass if r is not None: if r == '': - return httpwrappers.HttpResponseGone() - return httpwrappers.HttpResponsePermanentRedirect(r.new_path) + return http.HttpResponseGone() + return http.HttpResponsePermanentRedirect(r.new_path) # No redirect was found. Return the response. return response diff --git a/django/contrib/redirects/models/redirects.py b/django/contrib/redirects/models.py index 06dcdb38ac..3f01996b9c 100644 --- a/django/contrib/redirects/models/redirects.py +++ b/django/contrib/redirects/models.py @@ -1,23 +1,22 @@ -from django.core import meta -from django.models.core import Site +from django.db import models +from django.contrib.sites.models import Site from django.utils.translation import gettext_lazy as _ -class Redirect(meta.Model): - site = meta.ForeignKey(Site, radio_admin=meta.VERTICAL) - old_path = meta.CharField(_('redirect from'), maxlength=200, db_index=True, +class Redirect(models.Model): + site = models.ForeignKey(Site, radio_admin=models.VERTICAL) + old_path = models.CharField(_('redirect from'), maxlength=200, db_index=True, help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'.")) - new_path = meta.CharField(_('redirect to'), maxlength=200, blank=True, + new_path = models.CharField(_('redirect to'), maxlength=200, blank=True, help_text=_("This can be either an absolute path (as above) or a full URL starting with 'http://'.")) - class META: + class Meta: verbose_name = _('redirect') verbose_name_plural = _('redirects') - db_table = 'django_redirects' + db_table = 'django_redirect' unique_together=(('site', 'old_path'),) ordering = ('old_path',) - admin = meta.Admin( - list_filter = ('site',), - search_fields = ('old_path', 'new_path'), - ) + class Admin: + list_filter = ('site',) + search_fields = ('old_path', 'new_path') def __repr__(self): return "%s ---> %s" % (self.old_path, self.new_path) diff --git a/django/contrib/redirects/models/__init__.py b/django/contrib/redirects/models/__init__.py deleted file mode 100644 index 05063b2146..0000000000 --- a/django/contrib/redirects/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['redirects'] diff --git a/django/core/db/backends/__init__.py b/django/contrib/sessions/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/core/db/backends/__init__.py +++ b/django/contrib/sessions/__init__.py diff --git a/django/middleware/sessions.py b/django/contrib/sessions/middleware.py index e6fa7d742d..dae8a11554 100644 --- a/django/middleware/sessions.py +++ b/django/contrib/sessions/middleware.py @@ -1,5 +1,5 @@ -from django.conf.settings import SESSION_COOKIE_NAME, SESSION_COOKIE_AGE, SESSION_COOKIE_DOMAIN, SESSION_SAVE_EVERY_REQUEST -from django.models.core import sessions +from django.conf import settings +from django.contrib.sessions.models import Session from django.utils.cache import patch_vary_headers import datetime @@ -52,10 +52,10 @@ class SessionWrapper(object): self._session_cache = {} else: try: - s = sessions.get_object(session_key__exact=self.session_key, + s = Session.objects.get(session_key=self.session_key, expire_date__gt=datetime.datetime.now()) self._session_cache = s.get_decoded() - except sessions.SessionDoesNotExist: + except Session.DoesNotExist: self._session_cache = {} # Set the session_key to None to force creation of a new # key, for extra security. @@ -66,7 +66,7 @@ class SessionWrapper(object): class SessionMiddleware: def process_request(self, request): - request.session = SessionWrapper(request.COOKIES.get(SESSION_COOKIE_NAME, None)) + request.session = SessionWrapper(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)) def process_response(self, request, response): # If request.session was modified, or if response.session was set, save @@ -77,11 +77,11 @@ class SessionMiddleware: except AttributeError: pass else: - if modified or SESSION_SAVE_EVERY_REQUEST: - session_key = request.session.session_key or sessions.get_new_session_key() - new_session = sessions.save(session_key, request.session._session, - datetime.datetime.now() + datetime.timedelta(seconds=SESSION_COOKIE_AGE)) - expires = datetime.datetime.strftime(datetime.datetime.utcnow() + datetime.timedelta(seconds=SESSION_COOKIE_AGE), "%a, %d-%b-%Y %H:%M:%S GMT") - response.set_cookie(SESSION_COOKIE_NAME, session_key, - max_age=SESSION_COOKIE_AGE, expires=expires, domain=SESSION_COOKIE_DOMAIN) + if modified or settings.SESSION_SAVE_EVERY_REQUEST: + session_key = request.session.session_key or Session.objects.get_new_session_key() + new_session = Session.objects.save(session_key, request.session._session, + datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)) + expires = datetime.datetime.strftime(datetime.datetime.utcnow() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE), "%a, %d-%b-%Y %H:%M:%S GMT") + response.set_cookie(settings.SESSION_COOKIE_NAME, session_key, + max_age=settings.SESSION_COOKIE_AGE, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN) return response diff --git a/django/contrib/sessions/models.py b/django/contrib/sessions/models.py new file mode 100644 index 0000000000..8466495c86 --- /dev/null +++ b/django/contrib/sessions/models.py @@ -0,0 +1,55 @@ +import base64, md5, random, sys +import cPickle as pickle +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.conf import settings + +class SessionManager(models.Manager): + def encode(self, session_dict): + "Returns the given session dictionary pickled and encoded as a string." + pickled = pickle.dumps(session_dict) + pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest() + return base64.encodestring(pickled + pickled_md5) + + def get_new_session_key(self): + "Returns session key that isn't being used." + # The random module is seeded when this Apache child is created. + # Use person_id and SECRET_KEY as added salt. + while 1: + session_key = md5.new(str(random.randint(0, sys.maxint - 1)) + str(random.randint(0, sys.maxint - 1)) + settings.SECRET_KEY).hexdigest() + try: + self.get(session_key=session_key) + except self.model.DoesNotExist: + break + return session_key + + def save(self, session_key, session_dict, expire_date): + s = self.model(session_key, self.encode(session_dict), expire_date) + if session_dict: + s.save() + else: + s.delete() # Clear sessions with no data. + return s + +class Session(models.Model): + session_key = models.CharField(_('session key'), maxlength=40, primary_key=True) + session_data = models.TextField(_('session data')) + expire_date = models.DateTimeField(_('expire date')) + objects = SessionManager() + class Meta: + db_table = 'django_session' + verbose_name = _('session') + verbose_name_plural = _('sessions') + + def get_decoded(self): + encoded_data = base64.decodestring(self.session_data) + pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] + if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: + from django.core.exceptions import SuspiciousOperation + raise SuspiciousOperation, "User tampered with session cookie." + try: + return pickle.loads(pickled) + # Unpickling can cause a variety of exceptions. If something happens, + # just return an empty dictionary (an empty session). + except: + return {} diff --git a/django/core/template/loaders/__init__.py b/django/contrib/sites/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/core/template/loaders/__init__.py +++ b/django/contrib/sites/__init__.py diff --git a/django/contrib/sites/management.py b/django/contrib/sites/management.py new file mode 100644 index 0000000000..0e1a50227a --- /dev/null +++ b/django/contrib/sites/management.py @@ -0,0 +1,16 @@ +""" +Creates the default Site object. +""" + +from django.dispatch import dispatcher +from django.db.models import signals +from django.contrib.sites.models import Site +from django.contrib.sites import models as site_app + +def create_default_site(app, created_models): + if Site in created_models: + print "Creating example.com Site object" + s = Site(domain="example.com", name="example.com") + s.save() + +dispatcher.connect(create_default_site, sender=site_app, signal=signals.post_syncdb) diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py new file mode 100644 index 0000000000..861c3b26eb --- /dev/null +++ b/django/contrib/sites/models.py @@ -0,0 +1,23 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +class SiteManager(models.Manager): + def get_current(self): + from django.conf import settings + return self.get(pk=settings.SITE_ID) + +class Site(models.Model): + domain = models.CharField(_('domain name'), maxlength=100) + name = models.CharField(_('display name'), maxlength=50) + objects = SiteManager() + class Meta: + db_table = 'django_site' + verbose_name = _('site') + verbose_name_plural = _('sites') + ordering = ('domain',) + class Admin: + list_display = ('domain', 'name') + search_fields = ('domain', 'name') + + def __repr__(self): + return self.domain diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py index 8e3ccdb6c3..367af4f9ff 100644 --- a/django/contrib/syndication/feeds.py +++ b/django/contrib/syndication/feeds.py @@ -1,8 +1,8 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from django.core.template import Context, loader, Template, TemplateDoesNotExist -from django.models.core import sites +from django.template import Context, loader, Template, TemplateDoesNotExist +from django.contrib.sites.models import Site from django.utils import feedgenerator -from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE +from django.conf import settings def add_domain(domain, url): if not url.startswith('http://'): @@ -60,7 +60,7 @@ class Feed: else: obj = None - current_site = sites.get_current() + current_site = Site.objects.get_current() link = self.__get_dynamic_attr('link', obj) link = add_domain(current_site.domain, link) @@ -68,7 +68,7 @@ class Feed: title = self.__get_dynamic_attr('title', obj), link = link, description = self.__get_dynamic_attr('description', obj), - language = LANGUAGE_CODE.decode(), + language = settings.LANGUAGE_CODE.decode(), feed_url = add_domain(current_site, self.feed_url), author_name = self.__get_dynamic_attr('author_name', obj), author_link = self.__get_dynamic_attr('author_link', obj), @@ -76,11 +76,11 @@ class Feed: ) try: - title_template = loader.get_template('feeds/%s_title' % self.slug) + title_template = loader.get_template('feeds/%s_title.html' % self.slug) except TemplateDoesNotExist: title_template = Template('{{ obj }}') try: - description_template = loader.get_template('feeds/%s_description' % self.slug) + description_template = loader.get_template('feeds/%s_description.html' % self.slug) except TemplateDoesNotExist: description_template = Template('{{ obj }}') diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index 5117746f19..621665d4d4 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -1,6 +1,5 @@ from django.contrib.syndication import feeds -from django.core.exceptions import Http404 -from django.utils.httpwrappers import HttpResponse +from django.http import HttpResponse, Http404 def feed(request, url, feed_dict=None): if not feed_dict: diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 4fa79d8d79..1b54addded 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -1,7 +1,7 @@ "Database cache backend." from django.core.cache.backends.base import BaseCache -from django.core.db import db, DatabaseError +from django.db import connection, transaction import base64, time from datetime import datetime try: @@ -25,7 +25,7 @@ class CacheClass(BaseCache): self._cull_frequency = 3 def get(self, key, default=None): - cursor = db.cursor() + cursor = connection.cursor() cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key]) row = cursor.fetchone() if row is None: @@ -33,14 +33,14 @@ class CacheClass(BaseCache): now = datetime.now() if row[2] < now: cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) - db.commit() + transaction.commit_unless_managed() return default return pickle.loads(base64.decodestring(row[1])) def set(self, key, value, timeout=None): if timeout is None: timeout = self.default_timeout - cursor = db.cursor() + cursor = connection.cursor() cursor.execute("SELECT COUNT(*) FROM %s" % self._table) num = cursor.fetchone()[0] now = datetime.now().replace(microsecond=0) @@ -58,15 +58,15 @@ class CacheClass(BaseCache): # To be threadsafe, updates/inserts are allowed to fail silently pass else: - db.commit() + transaction.commit_unless_managed() def delete(self, key): - cursor = db.cursor() + cursor = connection.cursor() cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) - db.commit() + transaction.commit_unless_managed() def has_key(self, key): - cursor = db.cursor() + cursor = connection.cursor() cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s" % self._table, [key]) return cursor.fetchone() is not None diff --git a/django/core/context_processors.py b/django/core/context_processors.py index fc72aca6c8..1ab0768776 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -4,10 +4,10 @@ template context. Each function takes the request object as its only parameter and returns a dictionary to add to the context. These are referenced from the setting TEMPLATE_CONTEXT_PROCESSORS and used by -DjangoContext. +RequestContext. """ -from django.conf.settings import DEBUG, INTERNAL_IPS, LANGUAGES, LANGUAGE_CODE +from django.conf import settings def auth(request): """ @@ -23,19 +23,19 @@ def auth(request): def debug(request): "Returns context variables helpful for debugging." context_extras = {} - if DEBUG and request.META.get('REMOTE_ADDR') in INTERNAL_IPS: + if settings.DEBUG and request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: context_extras['debug'] = True - from django.core import db - context_extras['sql_queries'] = db.db.queries + from django.db import connection + context_extras['sql_queries'] = connection.queries return context_extras def i18n(request): context_extras = {} - context_extras['LANGUAGES'] = LANGUAGES + context_extras['LANGUAGES'] = settings.LANGUAGES if hasattr(request, 'LANGUAGE_CODE'): context_extras['LANGUAGE_CODE'] = request.LANGUAGE_CODE else: - context_extras['LANGUAGE_CODE'] = LANGUAGE_CODE + context_extras['LANGUAGE_CODE'] = settings.LANGUAGE_CODE return context_extras def request(request): diff --git a/django/core/db/__init__.py b/django/core/db/__init__.py deleted file mode 100644 index 1d2f04b367..0000000000 --- a/django/core/db/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -This is the core database connection. - -All Django code assumes database SELECT statements cast the resulting values as such: - * booleans are mapped to Python booleans - * dates are mapped to Python datetime.date objects - * times are mapped to Python datetime.time objects - * timestamps are mapped to Python datetime.datetime objects -""" - -from django.conf.settings import DATABASE_ENGINE - -try: - dbmod = __import__('django.core.db.backends.%s' % DATABASE_ENGINE, '', '', ['']) -except ImportError, exc: - # The database backend wasn't found. Display a helpful error message - # listing all possible database backends. - from django.core.exceptions import ImproperlyConfigured - import os - backend_dir = os.path.join(__path__[0], 'backends') - available_backends = [f[:-3] for f in os.listdir(backend_dir) if f.endswith('.py') and not f.startswith('__init__')] - available_backends.sort() - raise ImproperlyConfigured, "Could not load database backend: %s. Is your DATABASE_ENGINE setting (currently, %r) spelled correctly? Available options are: %s" % \ - (exc, DATABASE_ENGINE, ", ".join(map(repr, available_backends))) - -DatabaseError = dbmod.DatabaseError -db = dbmod.DatabaseWrapper() -dictfetchone = dbmod.dictfetchone -dictfetchmany = dbmod.dictfetchmany -dictfetchall = dbmod.dictfetchall -dictfetchall = dbmod.dictfetchall -get_last_insert_id = dbmod.get_last_insert_id -get_date_extract_sql = dbmod.get_date_extract_sql -get_date_trunc_sql = dbmod.get_date_trunc_sql -get_limit_offset_sql = dbmod.get_limit_offset_sql -get_random_function_sql = dbmod.get_random_function_sql -get_table_list = dbmod.get_table_list -get_table_description = dbmod.get_table_description -get_relations = dbmod.get_relations -get_indexes = dbmod.get_indexes -OPERATOR_MAPPING = dbmod.OPERATOR_MAPPING -DATA_TYPES = dbmod.DATA_TYPES -DATA_TYPES_REVERSE = dbmod.DATA_TYPES_REVERSE diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py deleted file mode 100644 index d0e27e6f44..0000000000 --- a/django/core/db/backends/mysql.py +++ /dev/null @@ -1,235 +0,0 @@ -""" -MySQL database backend for Django. - -Requires MySQLdb: http://sourceforge.net/projects/mysql-python -""" - -from django.core.db import base, typecasts -from django.core.db.dicthelpers import * -import MySQLdb as Database -from MySQLdb.converters import conversions -from MySQLdb.constants import FIELD_TYPE -import types - -DatabaseError = Database.DatabaseError - -django_conversions = conversions.copy() -django_conversions.update({ - types.BooleanType: typecasts.rev_typecast_boolean, - FIELD_TYPE.DATETIME: typecasts.typecast_timestamp, - FIELD_TYPE.DATE: typecasts.typecast_date, - FIELD_TYPE.TIME: typecasts.typecast_time, -}) - -# This is an extra debug layer over MySQL queries, to display warnings. -# It's only used when DEBUG=True. -class MysqlDebugWrapper: - def __init__(self, cursor): - self.cursor = cursor - - def execute(self, sql, params=()): - try: - return self.cursor.execute(sql, params) - except Database.Warning, w: - self.cursor.execute("SHOW WARNINGS") - raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) - - def executemany(self, sql, param_list): - try: - return self.cursor.executemany(sql, param_list) - except Database.Warning: - self.cursor.execute("SHOW WARNINGS") - raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) - - def __getattr__(self, attr): - if self.__dict__.has_key(attr): - return self.__dict__[attr] - else: - return getattr(self.cursor, attr) - -try: - # Only exists in python 2.4+ - from threading import local -except ImportError: - # Import copy of _thread_local.py from python 2.4 - from django.utils._threading_local import local - -class DatabaseWrapper(local): - def __init__(self): - self.connection = None - self.queries = [] - - def _valid_connection(self): - if self.connection is not None: - try: - self.connection.ping() - return True - except DatabaseError: - self.connection.close() - self.connection = None - return False - - def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG - if not self._valid_connection(): - kwargs = { - 'user': DATABASE_USER, - 'db': DATABASE_NAME, - 'passwd': DATABASE_PASSWORD, - 'host': DATABASE_HOST, - 'conv': django_conversions, - } - if DATABASE_PORT: - kwargs['port'] = DATABASE_PORT - self.connection = Database.connect(**kwargs) - cursor = self.connection.cursor() - if self.connection.get_server_info() >= '4.1': - cursor.execute("SET NAMES utf8") - if DEBUG: - return base.CursorDebugWrapper(MysqlDebugWrapper(cursor), self) - return cursor - - def commit(self): - self.connection.commit() - - def rollback(self): - if self.connection: - try: - self.connection.rollback() - except Database.NotSupportedError: - pass - - def close(self): - if self.connection is not None: - self.connection.close() - self.connection = None - - def quote_name(self, name): - if name.startswith("`") and name.endswith("`"): - return name # Quoting once is enough. - return "`%s`" % name - -def get_last_insert_id(cursor, table_name, pk_name): - return cursor.lastrowid - -def get_date_extract_sql(lookup_type, table_name): - # lookup_type is 'year', 'month', 'day' - # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html - return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), table_name) - -def get_date_trunc_sql(lookup_type, field_name): - # lookup_type is 'year', 'month', 'day' - # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html - # MySQL doesn't support DATE_TRUNC, so we fake it by subtracting intervals. - # If you know of a better way to do this, please file a Django ticket. - # Note that we can't use DATE_FORMAT directly because that causes the output - # to be a string rather than a datetime object, and we need MySQL to return - # a date so that it's typecasted properly into a Python datetime object. - subtractions = ["interval (DATE_FORMAT(%s, '%%%%s')) second - interval (DATE_FORMAT(%s, '%%%%i')) minute - interval (DATE_FORMAT(%s, '%%%%H')) hour" % (field_name, field_name, field_name)] - if lookup_type in ('year', 'month'): - subtractions.append(" - interval (DATE_FORMAT(%s, '%%%%e')-1) day" % field_name) - if lookup_type == 'year': - subtractions.append(" - interval (DATE_FORMAT(%s, '%%%%m')-1) month" % field_name) - return "(%s - %s)" % (field_name, ''.join(subtractions)) - -def get_limit_offset_sql(limit, offset=None): - sql = "LIMIT " - if offset and offset != 0: - sql += "%s," % offset - return sql + str(limit) - -def get_random_function_sql(): - return "RAND()" - -def get_table_list(cursor): - "Returns a list of table names in the current database." - cursor.execute("SHOW TABLES") - return [row[0] for row in cursor.fetchall()] - -def get_table_description(cursor, table_name): - "Returns a description of the table, with the DB-API cursor.description interface." - cursor.execute("SELECT * FROM %s LIMIT 1" % DatabaseWrapper().quote_name(table_name)) - return cursor.description - -def get_relations(cursor, table_name): - raise NotImplementedError - -def get_indexes(cursor, table_name): - """ - Returns a dictionary of fieldname -> infodict for the given table, - where each infodict is in the format: - {'primary_key': boolean representing whether it's the primary key, - 'unique': boolean representing whether it's a unique index} - """ - cursor.execute("SHOW INDEX FROM %s" % DatabaseWrapper().quote_name(table_name)) - indexes = {} - for row in cursor.fetchall(): - indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])} - return indexes - -OPERATOR_MAPPING = { - 'exact': '= %s', - 'iexact': 'LIKE %s', - 'contains': 'LIKE BINARY %s', - 'icontains': 'LIKE %s', - 'ne': '!= %s', - 'gt': '> %s', - 'gte': '>= %s', - 'lt': '< %s', - 'lte': '<= %s', - 'startswith': 'LIKE BINARY %s', - 'endswith': 'LIKE BINARY %s', - 'istartswith': 'LIKE %s', - 'iendswith': 'LIKE %s', -} - -# This dictionary maps Field objects to their associated MySQL column -# types, as strings. Column-type strings can contain format strings; they'll -# be interpolated against the values of Field.__dict__ before being output. -# If a column type is set to None, it won't be included in the output. -DATA_TYPES = { - 'AutoField': 'integer AUTO_INCREMENT', - 'BooleanField': 'bool', - 'CharField': 'varchar(%(maxlength)s)', - 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', - 'DateField': 'date', - 'DateTimeField': 'datetime', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', - 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'ImageField': 'varchar(100)', - 'IntegerField': 'integer', - 'IPAddressField': 'char(15)', - 'ManyToManyField': None, - 'NullBooleanField': 'bool', - 'OneToOneField': 'integer', - 'PhoneNumberField': 'varchar(20)', - 'PositiveIntegerField': 'integer UNSIGNED', - 'PositiveSmallIntegerField': 'smallint UNSIGNED', - 'SlugField': 'varchar(%(maxlength)s)', - 'SmallIntegerField': 'smallint', - 'TextField': 'longtext', - 'TimeField': 'time', - 'URLField': 'varchar(200)', - 'USStateField': 'varchar(2)', -} - -DATA_TYPES_REVERSE = { - FIELD_TYPE.BLOB: 'TextField', - FIELD_TYPE.CHAR: 'CharField', - FIELD_TYPE.DECIMAL: 'FloatField', - FIELD_TYPE.DATE: 'DateField', - FIELD_TYPE.DATETIME: 'DateTimeField', - FIELD_TYPE.DOUBLE: 'FloatField', - FIELD_TYPE.FLOAT: 'FloatField', - FIELD_TYPE.INT24: 'IntegerField', - FIELD_TYPE.LONG: 'IntegerField', - FIELD_TYPE.LONGLONG: 'IntegerField', - FIELD_TYPE.SHORT: 'IntegerField', - FIELD_TYPE.STRING: 'TextField', - FIELD_TYPE.TIMESTAMP: 'DateTimeField', - FIELD_TYPE.TINY_BLOB: 'TextField', - FIELD_TYPE.MEDIUM_BLOB: 'TextField', - FIELD_TYPE.LONG_BLOB: 'TextField', - FIELD_TYPE.VAR_STRING: 'CharField', -} diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py deleted file mode 100644 index 0bc799c247..0000000000 --- a/django/core/db/backends/postgresql.py +++ /dev/null @@ -1,238 +0,0 @@ -""" -PostgreSQL database backend for Django. - -Requires psycopg 1: http://initd.org/projects/psycopg1 -""" - -from django.core.db import base, typecasts -import psycopg as Database - -DatabaseError = Database.DatabaseError - -try: - # Only exists in python 2.4+ - from threading import local -except ImportError: - # Import copy of _thread_local.py from python 2.4 - from django.utils._threading_local import local - -class DatabaseWrapper(local): - def __init__(self): - self.connection = None - self.queries = [] - - def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, TIME_ZONE - if self.connection is None: - if DATABASE_NAME == '': - from django.core.exceptions import ImproperlyConfigured - raise ImproperlyConfigured, "You need to specify DATABASE_NAME in your Django settings file." - conn_string = "dbname=%s" % DATABASE_NAME - if DATABASE_USER: - conn_string = "user=%s %s" % (DATABASE_USER, conn_string) - if DATABASE_PASSWORD: - conn_string += " password='%s'" % DATABASE_PASSWORD - if DATABASE_HOST: - conn_string += " host=%s" % DATABASE_HOST - if DATABASE_PORT: - conn_string += " port=%s" % DATABASE_PORT - self.connection = Database.connect(conn_string) - self.connection.set_isolation_level(1) # make transactions transparent to all cursors - cursor = self.connection.cursor() - cursor.execute("SET TIME ZONE %s", [TIME_ZONE]) - if DEBUG: - return base.CursorDebugWrapper(cursor, self) - return cursor - - def commit(self): - return self.connection.commit() - - def rollback(self): - if self.connection: - return self.connection.rollback() - - def close(self): - if self.connection is not None: - self.connection.close() - self.connection = None - - def quote_name(self, name): - if name.startswith('"') and name.endswith('"'): - return name # Quoting once is enough. - return '"%s"' % name - -def dictfetchone(cursor): - "Returns a row from the cursor as a dict" - return cursor.dictfetchone() - -def dictfetchmany(cursor, number): - "Returns a certain number of rows from a cursor as a dict" - return cursor.dictfetchmany(number) - -def dictfetchall(cursor): - "Returns all rows from a cursor as a dict" - return cursor.dictfetchall() - -def get_last_insert_id(cursor, table_name, pk_name): - cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name)) - return cursor.fetchone()[0] - -def get_date_extract_sql(lookup_type, table_name): - # lookup_type is 'year', 'month', 'day' - # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT - return "EXTRACT('%s' FROM %s)" % (lookup_type, table_name) - -def get_date_trunc_sql(lookup_type, field_name): - # lookup_type is 'year', 'month', 'day' - # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC - return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) - -def get_limit_offset_sql(limit, offset=None): - sql = "LIMIT %s" % limit - if offset and offset != 0: - sql += " OFFSET %s" % offset - return sql - -def get_random_function_sql(): - return "RANDOM()" - -def get_table_list(cursor): - "Returns a list of table names in the current database." - cursor.execute(""" - SELECT c.relname - FROM pg_catalog.pg_class c - LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r', 'v', '') - AND n.nspname NOT IN ('pg_catalog', 'pg_toast') - AND pg_catalog.pg_table_is_visible(c.oid)""") - return [row[0] for row in cursor.fetchall()] - -def get_table_description(cursor, table_name): - "Returns a description of the table, with the DB-API cursor.description interface." - cursor.execute("SELECT * FROM %s LIMIT 1" % DatabaseWrapper().quote_name(table_name)) - return cursor.description - -def get_relations(cursor, table_name): - """ - Returns a dictionary of {field_index: (field_index_other_table, other_table)} - representing all relationships to the given table. Indexes are 0-based. - """ - cursor.execute(""" - SELECT con.conkey, con.confkey, c2.relname - FROM pg_constraint con, pg_class c1, pg_class c2 - WHERE c1.oid = con.conrelid - AND c2.oid = con.confrelid - AND c1.relname = %s - AND con.contype = 'f'""", [table_name]) - relations = {} - for row in cursor.fetchall(): - try: - # row[0] and row[1] are like "{2}", so strip the curly braces. - relations[int(row[0][1:-1]) - 1] = (int(row[1][1:-1]) - 1, row[2]) - except ValueError: - continue - return relations - -def get_indexes(cursor, table_name): - """ - Returns a dictionary of fieldname -> infodict for the given table, - where each infodict is in the format: - {'primary_key': boolean representing whether it's the primary key, - 'unique': boolean representing whether it's a unique index} - """ - # Get the table description because we only have the column indexes, and we - # need the column names. - desc = get_table_description(cursor, table_name) - # This query retrieves each index on the given table. - cursor.execute(""" - SELECT idx.indkey, idx.indisunique, idx.indisprimary - FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, - pg_catalog.pg_index idx - WHERE c.oid = idx.indrelid - AND idx.indexrelid = c2.oid - AND c.relname = %s""", [table_name]) - indexes = {} - for row in cursor.fetchall(): - # row[0] (idx.indkey) is stored in the DB as an array. It comes out as - # a string of space-separated integers. This designates the field - # indexes (1-based) of the fields that have indexes on the table. - # Here, we skip any indexes across multiple fields. - if ' ' in row[0]: - continue - col_name = desc[int(row[0])-1][0] - indexes[col_name] = {'primary_key': row[2], 'unique': row[1]} - return indexes - -# Register these custom typecasts, because Django expects dates/times to be -# in Python's native (standard-library) datetime/time format, whereas psycopg -# use mx.DateTime by default. -try: - Database.register_type(Database.new_type((1082,), "DATE", typecasts.typecast_date)) -except AttributeError: - raise Exception, "You appear to be using psycopg version 2, which isn't supported yet, because it's still in beta. Use psycopg version 1 instead: http://initd.org/projects/psycopg1" -Database.register_type(Database.new_type((1083,1266), "TIME", typecasts.typecast_time)) -Database.register_type(Database.new_type((1114,1184), "TIMESTAMP", typecasts.typecast_timestamp)) -Database.register_type(Database.new_type((16,), "BOOLEAN", typecasts.typecast_boolean)) - -OPERATOR_MAPPING = { - 'exact': '= %s', - 'iexact': 'ILIKE %s', - 'contains': 'LIKE %s', - 'icontains': 'ILIKE %s', - 'ne': '!= %s', - 'gt': '> %s', - 'gte': '>= %s', - 'lt': '< %s', - 'lte': '<= %s', - 'startswith': 'LIKE %s', - 'endswith': 'LIKE %s', - 'istartswith': 'ILIKE %s', - 'iendswith': 'ILIKE %s', -} - -# This dictionary maps Field objects to their associated PostgreSQL column -# types, as strings. Column-type strings can contain format strings; they'll -# be interpolated against the values of Field.__dict__ before being output. -# If a column type is set to None, it won't be included in the output. -DATA_TYPES = { - 'AutoField': 'serial', - 'BooleanField': 'boolean', - 'CharField': 'varchar(%(maxlength)s)', - 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', - 'DateField': 'date', - 'DateTimeField': 'timestamp with time zone', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', - 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'ImageField': 'varchar(100)', - 'IntegerField': 'integer', - 'IPAddressField': 'inet', - 'ManyToManyField': None, - 'NullBooleanField': 'boolean', - 'OneToOneField': 'integer', - 'PhoneNumberField': 'varchar(20)', - 'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)', - 'PositiveSmallIntegerField': 'smallint CHECK ("%(column)s" >= 0)', - 'SlugField': 'varchar(%(maxlength)s)', - 'SmallIntegerField': 'smallint', - 'TextField': 'text', - 'TimeField': 'time', - 'URLField': 'varchar(200)', - 'USStateField': 'varchar(2)', -} - -# Maps type codes to Django Field types. -DATA_TYPES_REVERSE = { - 16: 'BooleanField', - 21: 'SmallIntegerField', - 23: 'IntegerField', - 25: 'TextField', - 869: 'IPAddressField', - 1043: 'CharField', - 1082: 'DateField', - 1083: 'TimeField', - 1114: 'DateTimeField', - 1184: 'DateTimeField', - 1266: 'TimeField', - 1700: 'FloatField', -} diff --git a/django/core/db/backends/sqlite3.py b/django/core/db/backends/sqlite3.py deleted file mode 100644 index 1b2ae8cd84..0000000000 --- a/django/core/db/backends/sqlite3.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -SQLite3 backend for django. Requires pysqlite2 (http://pysqlite.org/). -""" - -from django.core.db import base, typecasts -from django.core.db.dicthelpers import * -from pysqlite2 import dbapi2 as Database -DatabaseError = Database.DatabaseError - -# Register adaptors ########################################################### - -Database.register_converter("bool", lambda s: str(s) == '1') -Database.register_converter("time", typecasts.typecast_time) -Database.register_converter("date", typecasts.typecast_date) -Database.register_converter("datetime", typecasts.typecast_timestamp) - -# Database wrapper ############################################################ - -def utf8rowFactory(cursor, row): - def utf8(s): - if type(s) == unicode: - return s.encode("utf-8") - else: - return s - return [utf8(r) for r in row] - -try: - # Only exists in python 2.4+ - from threading import local -except ImportError: - # Import copy of _thread_local.py from python 2.4 - from django.utils._threading_local import local - -class DatabaseWrapper(local): - def __init__(self): - self.connection = None - self.queries = [] - - def cursor(self): - from django.conf.settings import DATABASE_NAME, DEBUG - if self.connection is None: - self.connection = Database.connect(DATABASE_NAME, detect_types=Database.PARSE_DECLTYPES) - # register extract and date_trun functions - self.connection.create_function("django_extract", 2, _sqlite_extract) - self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) - cursor = self.connection.cursor(factory=SQLiteCursorWrapper) - cursor.row_factory = utf8rowFactory - if DEBUG: - return base.CursorDebugWrapper(cursor, self) - else: - return cursor - - def commit(self): - self.connection.commit() - - def rollback(self): - if self.connection: - self.connection.rollback() - - def close(self): - if self.connection is not None: - self.connection.close() - self.connection = None - - def quote_name(self, name): - if name.startswith('"') and name.endswith('"'): - return name # Quoting once is enough. - return '"%s"' % name - -class SQLiteCursorWrapper(Database.Cursor): - """ - Django uses "format" style placeholders, but pysqlite2 uses "qmark" style. - This fixes it -- but note that if you want to use a literal "%s" in a query, - you'll need to use "%%s" (which I belive is true of other wrappers as well). - """ - - def execute(self, query, params=[]): - query = self.convert_query(query, len(params)) - return Database.Cursor.execute(self, query, params) - - def executemany(self, query, params=[]): - query = self.convert_query(query, len(params[0])) - return Database.Cursor.executemany(self, query, params) - - def convert_query(self, query, num_params): - # XXX this seems too simple to be correct... is this right? - return query % tuple("?" * num_params) - -# Helper functions ############################################################ - -def get_last_insert_id(cursor, table_name, pk_name): - return cursor.lastrowid - -def get_date_extract_sql(lookup_type, table_name): - # lookup_type is 'year', 'month', 'day' - # sqlite doesn't support extract, so we fake it with the user-defined - # function _sqlite_extract that's registered in connect(), above. - return 'django_extract("%s", %s)' % (lookup_type.lower(), table_name) - -def _sqlite_extract(lookup_type, dt): - try: - dt = typecasts.typecast_timestamp(dt) - except (ValueError, TypeError): - return None - return str(getattr(dt, lookup_type)) - -def get_date_trunc_sql(lookup_type, field_name): - # lookup_type is 'year', 'month', 'day' - # sqlite doesn't support DATE_TRUNC, so we fake it as above. - return 'django_date_trunc("%s", %s)' % (lookup_type.lower(), field_name) - -def get_limit_offset_sql(limit, offset=None): - sql = "LIMIT %s" % limit - if offset and offset != 0: - sql += " OFFSET %s" % offset - return sql - -def get_random_function_sql(): - return "RANDOM()" - -def _sqlite_date_trunc(lookup_type, dt): - try: - dt = typecasts.typecast_timestamp(dt) - except (ValueError, TypeError): - return None - if lookup_type == 'year': - return "%i-01-01 00:00:00" % dt.year - elif lookup_type == 'month': - return "%i-%02i-01 00:00:00" % (dt.year, dt.month) - elif lookup_type == 'day': - return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) - -def get_table_list(cursor): - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") - return [row[0] for row in cursor.fetchall()] - -def get_table_description(cursor, table_name): - cursor.execute("PRAGMA table_info(%s)" % DatabaseWrapper().quote_name(table_name)) - return [(row[1], row[2], None, None) for row in cursor.fetchall()] - -def get_relations(cursor, table_name): - raise NotImplementedError - -def get_indexes(cursor, table_name): - raise NotImplementedError - -# Operators and fields ######################################################## - -# SQLite requires LIKE statements to include an ESCAPE clause if the value -# being escaped has a percent or underscore in it. -# See http://www.sqlite.org/lang_expr.html for an explanation. -OPERATOR_MAPPING = { - 'exact': '= %s', - 'iexact': "LIKE %s ESCAPE '\\'", - 'contains': "LIKE %s ESCAPE '\\'", - 'icontains': "LIKE %s ESCAPE '\\'", - 'ne': '!= %s', - 'gt': '> %s', - 'gte': '>= %s', - 'lt': '< %s', - 'lte': '<= %s', - 'startswith': "LIKE %s ESCAPE '\\'", - 'endswith': "LIKE %s ESCAPE '\\'", - 'istartswith': "LIKE %s ESCAPE '\\'", - 'iendswith': "LIKE %s ESCAPE '\\'", -} - -# SQLite doesn't actually support most of these types, but it "does the right -# thing" given more verbose field definitions, so leave them as is so that -# schema inspection is more useful. -DATA_TYPES = { - 'AutoField': 'integer', - 'BooleanField': 'bool', - 'CharField': 'varchar(%(maxlength)s)', - 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', - 'DateField': 'date', - 'DateTimeField': 'datetime', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', - 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'ImageField': 'varchar(100)', - 'IntegerField': 'integer', - 'IPAddressField': 'char(15)', - 'ManyToManyField': None, - 'NullBooleanField': 'bool', - 'OneToOneField': 'integer', - 'PhoneNumberField': 'varchar(20)', - 'PositiveIntegerField': 'integer unsigned', - 'PositiveSmallIntegerField': 'smallint unsigned', - 'SlugField': 'varchar(%(maxlength)s)', - 'SmallIntegerField': 'smallint', - 'TextField': 'text', - 'TimeField': 'time', - 'URLField': 'varchar(200)', - 'USStateField': 'varchar(2)', -} - -# Maps SQL types to Django Field types. Some of the SQL types have multiple -# entries here because SQLite allows for anything and doesn't normalize the -# field type; it uses whatever was given. -BASE_DATA_TYPES_REVERSE = { - 'bool': 'BooleanField', - 'boolean': 'BooleanField', - 'smallint': 'SmallIntegerField', - 'smallinteger': 'SmallIntegerField', - 'int': 'IntegerField', - 'integer': 'IntegerField', - 'text': 'TextField', - 'char': 'CharField', - 'date': 'DateField', - 'datetime': 'DateTimeField', - 'time': 'TimeField', -} - -# This light wrapper "fakes" a dictionary interface, because some SQLite data -# types include variables in them -- e.g. "varchar(30)" -- and can't be matched -# as a simple dictionary lookup. -class FlexibleFieldLookupDict: - def __getitem__(self, key): - key = key.lower() - try: - return BASE_DATA_TYPES_REVERSE[key] - except KeyError: - import re - m = re.search(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$', key) - if m: - return ('CharField', {'maxlength': int(m.group(1))}) - raise KeyError - -DATA_TYPES_REVERSE = FlexibleFieldLookupDict() diff --git a/django/core/db/base.py b/django/core/db/base.py deleted file mode 100644 index 62cca9d0be..0000000000 --- a/django/core/db/base.py +++ /dev/null @@ -1,32 +0,0 @@ -from time import time - -class CursorDebugWrapper: - def __init__(self, cursor, db): - self.cursor = cursor - self.db = db - - def execute(self, sql, params=[]): - start = time() - result = self.cursor.execute(sql, params) - stop = time() - self.db.queries.append({ - 'sql': sql % tuple(params), - 'time': "%.3f" % (stop - start), - }) - return result - - def executemany(self, sql, param_list): - start = time() - result = self.cursor.executemany(sql, param_list) - stop = time() - self.db.queries.append({ - 'sql': 'MANY: ' + sql + ' ' + str(tuple(param_list)), - 'time': "%.3f" % (stop - start), - }) - return result - - def __getattr__(self, attr): - if self.__dict__.has_key(attr): - return self.__dict__[attr] - else: - return getattr(self.cursor, attr) diff --git a/django/core/db/dicthelpers.py b/django/core/db/dicthelpers.py deleted file mode 100644 index 5aedc51aed..0000000000 --- a/django/core/db/dicthelpers.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Helper functions for dictfetch* for databases that don't natively support them. -""" - -def _dict_helper(desc, row): - "Returns a dictionary for the given cursor.description and result row." - return dict([(desc[col[0]][0], col[1]) for col in enumerate(row)]) - -def dictfetchone(cursor): - "Returns a row from the cursor as a dict" - row = cursor.fetchone() - if not row: - return None - return _dict_helper(cursor.description, row) - -def dictfetchmany(cursor, number): - "Returns a certain number of rows from a cursor as a dict" - desc = cursor.description - return [_dict_helper(desc, row) for row in cursor.fetchmany(number)] - -def dictfetchall(cursor): - "Returns all rows from a cursor as a dict" - desc = cursor.description - return [_dict_helper(desc, row) for row in cursor.fetchall()] diff --git a/django/core/db/typecasts.py b/django/core/db/typecasts.py deleted file mode 100644 index 9be9062626..0000000000 --- a/django/core/db/typecasts.py +++ /dev/null @@ -1,55 +0,0 @@ -import datetime - -############################################### -# Converters from database (string) to Python # -############################################### - -def typecast_date(s): - return s and datetime.date(*map(int, s.split('-'))) or None # returns None if s is null - -def typecast_time(s): # does NOT store time zone information - if not s: return None - hour, minutes, seconds = s.split(':') - if '.' in seconds: # check whether seconds have a fractional part - seconds, microseconds = seconds.split('.') - else: - microseconds = '0' - return datetime.time(int(hour), int(minutes), int(seconds), int(float('.'+microseconds) * 1000000)) - -def typecast_timestamp(s): # does NOT store time zone information - # "2005-07-29 15:48:00.590358-05" - # "2005-07-29 09:56:00-05" - if not s: return None - if not ' ' in s: return typecast_date(s) - d, t = s.split() - # Extract timezone information, if it exists. Currently we just throw - # it away, but in the future we may make use of it. - if '-' in t: - t, tz = t.split('-', 1) - tz = '-' + tz - elif '+' in t: - t, tz = t.split('+', 1) - tz = '+' + tz - else: - tz = '' - dates = d.split('-') - times = t.split(':') - seconds = times[2] - if '.' in seconds: # check whether seconds have a fractional part - seconds, microseconds = seconds.split('.') - else: - microseconds = '0' - return datetime.datetime(int(dates[0]), int(dates[1]), int(dates[2]), - int(times[0]), int(times[1]), int(seconds), int(float('.'+microseconds) * 1000000)) - -def typecast_boolean(s): - if s is None: return None - if not s: return False - return str(s)[0].lower() == 't' - -############################################### -# Converters from Python to database (string) # -############################################### - -def rev_typecast_boolean(obj, d): - return obj and '1' or '0' diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 9d1aab665f..f22f67c261 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -1,13 +1,8 @@ "Global Django exceptions" -from django.core.template import SilentVariableFailure - -class Http404(Exception): - pass - -class ObjectDoesNotExist(SilentVariableFailure): +class ObjectDoesNotExist(Exception): "The requested object does not exist" - pass + silent_variable_failure = True class SuspiciousOperation(Exception): "The user did something suspicious" diff --git a/django/core/extensions.py b/django/core/extensions.py deleted file mode 100644 index c03f63addc..0000000000 --- a/django/core/extensions.py +++ /dev/null @@ -1,82 +0,0 @@ -# This module collects helper functions and classes that "span" multiple levels -# of MVC. In other words, these functions/classes introduce controlled coupling -# for convenience's sake. - -from django.core.exceptions import Http404, ImproperlyConfigured, ObjectDoesNotExist -from django.core.template import Context, loader -from django.conf.settings import TEMPLATE_CONTEXT_PROCESSORS -from django.utils.httpwrappers import HttpResponse - -_standard_context_processors = None - -# This is a function rather than module-level procedural code because we only -# want it to execute if somebody uses DjangoContext. -def get_standard_processors(): - global _standard_context_processors - if _standard_context_processors is None: - processors = [] - for path in TEMPLATE_CONTEXT_PROCESSORS: - i = path.rfind('.') - module, attr = path[:i], path[i+1:] - try: - mod = __import__(module, '', '', [attr]) - except ImportError, e: - raise ImproperlyConfigured, 'Error importing request processor module %s: "%s"' % (module, e) - try: - func = getattr(mod, attr) - except AttributeError: - raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable request processor' % (module, attr) - processors.append(func) - _standard_context_processors = tuple(processors) - return _standard_context_processors - -def render_to_response(*args, **kwargs): - return HttpResponse(loader.render_to_string(*args, **kwargs)) -load_and_render = render_to_response # For backwards compatibility. - -def get_object_or_404(mod, **kwargs): - try: - return mod.get_object(**kwargs) - except ObjectDoesNotExist: - raise Http404 - -def get_list_or_404(mod, **kwargs): - obj_list = mod.get_list(**kwargs) - if not obj_list: - raise Http404 - return obj_list - -class DjangoContext(Context): - """ - This subclass of template.Context automatically populates itself using - the processors defined in TEMPLATE_CONTEXT_PROCESSORS. - Additional processors can be specified as a list of callables - using the "processors" keyword argument. - """ - def __init__(self, request, dict=None, processors=None): - Context.__init__(self, dict) - if processors is None: - processors = () - else: - processors = tuple(processors) - for processor in get_standard_processors() + processors: - self.update(processor(request)) - -# PermWrapper and PermLookupDict proxy the permissions system into objects that -# the template system can understand. - -class PermLookupDict: - def __init__(self, user, module_name): - self.user, self.module_name = user, module_name - def __repr__(self): - return str(self.user.get_permission_list()) - def __getitem__(self, perm_name): - return self.user.has_perm("%s.%s" % (self.module_name, perm_name)) - def __nonzero__(self): - return self.user.has_module_perms(self.module_name) - -class PermWrapper: - def __init__(self, user): - self.user = user - def __getitem__(self, module_name): - return PermLookupDict(self.user, module_name) diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index fd3a3ccae1..fce724c31f 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -1,4 +1,6 @@ -from django.utils import httpwrappers +from django.core import signals +from django.dispatch import dispatcher +from django import http import sys class BaseHandler: @@ -48,12 +50,9 @@ class BaseHandler: def get_response(self, path, request): "Returns an HttpResponse object for the given HttpRequest" - from django.core import db, exceptions, urlresolvers + from django.core import exceptions, urlresolvers from django.core.mail import mail_admins - from django.conf.settings import DEBUG, INTERNAL_IPS, ROOT_URLCONF - - # Reset query list per request. - db.db.queries = [] + from django.conf import settings # Apply request middleware for middleware_method in self._request_middleware: @@ -61,7 +60,7 @@ class BaseHandler: if response: return response - resolver = urlresolvers.RegexURLResolver(r'^/', ROOT_URLCONF) + resolver = urlresolvers.RegexURLResolver(r'^/', settings.ROOT_URLCONF) try: callback, callback_args, callback_kwargs = resolver.resolve(path) @@ -88,30 +87,23 @@ class BaseHandler: raise ValueError, "The view %s.%s didn't return an HttpResponse object." % (callback.__module__, callback.func_name) return response - except exceptions.Http404, e: - if DEBUG: + except http.Http404, e: + if settings.DEBUG: return self.get_technical_error_response(request, is404=True, exception=e) else: callback, param_dict = resolver.resolve404() return callback(request, **param_dict) - except db.DatabaseError: - db.db.rollback() - if DEBUG: - return self.get_technical_error_response(request) - else: - subject = 'Database error (%s IP): %s' % ((request.META.get('REMOTE_ADDR') in INTERNAL_IPS and 'internal' or 'EXTERNAL'), getattr(request, 'path', '')) - message = "%s\n\n%s" % (self._get_traceback(), request) - mail_admins(subject, message, fail_silently=True) - return self.get_friendly_error_response(request, resolver) except exceptions.PermissionDenied: - return httpwrappers.HttpResponseForbidden('<h1>Permission denied</h1>') + return http.HttpResponseForbidden('<h1>Permission denied</h1>') except: # Handle everything else, including SuspiciousOperation, etc. - if DEBUG: + if settings.DEBUG: return self.get_technical_error_response(request) else: # Get the exception info now, in case another exception is thrown later. exc_info = sys.exc_info() - subject = 'Coding error (%s IP): %s' % ((request.META.get('REMOTE_ADDR') in INTERNAL_IPS and 'internal' or 'EXTERNAL'), getattr(request, 'path', '')) + receivers = dispatcher.send(signal=signals.got_request_exception) + # When DEBUG is False, send an error message to the admins. + subject = 'Error (%s IP): %s' % ((request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'), getattr(request, 'path', '')) try: request_repr = repr(request) except: @@ -123,7 +115,7 @@ class BaseHandler: def get_friendly_error_response(self, request, resolver): """ Returns an HttpResponse that displays a PUBLIC error message for a - fundamental database or coding error. + fundamental error. """ from django.core import urlresolvers callback, param_dict = resolver.resolve500() @@ -132,7 +124,7 @@ class BaseHandler: def get_technical_error_response(self, request, is404=False, exception=None): """ Returns an HttpResponse that displays a TECHNICAL error message for a - fundamental database or coding error. + fundamental error. """ from django.views import debug if is404: diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index 37499c6f06..ce7f6f1ad2 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -1,5 +1,8 @@ from django.core.handlers.base import BaseHandler -from django.utils import datastructures, httpwrappers +from django.core import signals +from django.dispatch import dispatcher +from django.utils import datastructures +from django import http from pprint import pformat import os @@ -7,15 +10,15 @@ import os # settings) until after ModPythonHandler has been called; otherwise os.environ # won't be set up correctly (with respect to settings). -class ModPythonRequest(httpwrappers.HttpRequest): +class ModPythonRequest(http.HttpRequest): def __init__(self, req): self._req = req self.path = req.uri def __repr__(self): - return '<ModPythonRequest\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s,\nuser:%s>' % \ + return '<ModPythonRequest\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \ (self.path, pformat(self.GET), pformat(self.POST), pformat(self.COOKIES), - pformat(self.META), pformat(self.user)) + pformat(self.META)) def get_full_path(self): return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '') @@ -23,18 +26,18 @@ class ModPythonRequest(httpwrappers.HttpRequest): def _load_post_and_files(self): "Populates self._post and self._files" if self._req.headers_in.has_key('content-type') and self._req.headers_in['content-type'].startswith('multipart'): - self._post, self._files = httpwrappers.parse_file_upload(self._req.headers_in, self.raw_post_data) + self._post, self._files = http.parse_file_upload(self._req.headers_in, self.raw_post_data) else: - self._post, self._files = httpwrappers.QueryDict(self.raw_post_data), datastructures.MultiValueDict() + self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict() def _get_request(self): if not hasattr(self, '_request'): - self._request = datastructures.MergeDict(self.POST, self.GET) + self._request = datastructures.MergeDict(self.POST, self.GET) return self._request def _get_get(self): if not hasattr(self, '_get'): - self._get = httpwrappers.QueryDict(self._req.args) + self._get = http.QueryDict(self._req.args) return self._get def _set_get(self, get): @@ -50,7 +53,7 @@ class ModPythonRequest(httpwrappers.HttpRequest): def _get_cookies(self): if not hasattr(self, '_cookies'): - self._cookies = httpwrappers.parse_cookie(self._req.headers_in.get('cookie', '')) + self._cookies = http.parse_cookie(self._req.headers_in.get('cookie', '')) return self._cookies def _set_cookies(self, cookies): @@ -95,22 +98,6 @@ class ModPythonRequest(httpwrappers.HttpRequest): self._raw_post_data = self._req.read() return self._raw_post_data - def _get_user(self): - if not hasattr(self, '_user'): - from django.models.auth import users - try: - user_id = self.session[users.SESSION_KEY] - if not user_id: - raise ValueError - self._user = users.get_object(pk=user_id) - except (AttributeError, KeyError, ValueError, users.UserDoesNotExist): - from django.parts.auth import anonymoususers - self._user = anonymoususers.AnonymousUser() - return self._user - - def _set_user(self, user): - self._user = user - GET = property(_get_get, _set_get) POST = property(_get_post, _set_post) COOKIES = property(_get_cookies, _set_cookies) @@ -118,7 +105,6 @@ class ModPythonRequest(httpwrappers.HttpRequest): META = property(_get_meta) REQUEST = property(_get_request) raw_post_data = property(_get_raw_post_data) - user = property(_get_user, _set_user) class ModPythonHandler(BaseHandler): def __call__(self, req): @@ -128,7 +114,6 @@ class ModPythonHandler(BaseHandler): # now that the environ works we can see the correct settings, so imports # that use settings now can work from django.conf import settings - from django.core import db if settings.ENABLE_PSYCO: import psyco @@ -138,15 +123,17 @@ class ModPythonHandler(BaseHandler): if self._request_middleware is None: self.load_middleware() + dispatcher.send(signal=signals.request_started) try: request = ModPythonRequest(req) response = self.get_response(req.uri, request) - finally: - db.db.close() - # Apply response middleware - for middleware_method in self._response_middleware: - response = middleware_method(request, response) + # Apply response middleware + for middleware_method in self._response_middleware: + response = middleware_method(request, response) + + finally: + dispatcher.send(signal=signals.request_finished) # Convert our custom HttpResponse object back into the mod_python req. populate_apache_request(response, req) diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 7541a5fd9d..01dbdf02f1 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -1,5 +1,8 @@ from django.core.handlers.base import BaseHandler -from django.utils import datastructures, httpwrappers +from django.core import signals +from django.dispatch import dispatcher +from django.utils import datastructures +from django import http from pprint import pformat # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html @@ -47,7 +50,7 @@ STATUS_CODE_TEXT = { 505: 'HTTP VERSION NOT SUPPORTED', } -class WSGIRequest(httpwrappers.HttpRequest): +class WSGIRequest(http.HttpRequest): def __init__(self, environ): self.environ = environ self.path = environ['PATH_INFO'] @@ -60,7 +63,7 @@ class WSGIRequest(httpwrappers.HttpRequest): pformat(self.META)) def get_full_path(self): - return '%s%s' % (self.path, self.environ['QUERY_STRING'] and ('?' + self.environ['QUERY_STRING']) or '') + return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '') def _load_post_and_files(self): # Populates self._post and self._files @@ -68,21 +71,21 @@ class WSGIRequest(httpwrappers.HttpRequest): if self.environ.get('CONTENT_TYPE', '').startswith('multipart'): header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')]) header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '') - self._post, self._files = httpwrappers.parse_file_upload(header_dict, self.raw_post_data) + self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data) else: - self._post, self._files = httpwrappers.QueryDict(self.raw_post_data), datastructures.MultiValueDict() + self._post, self._files = http.QueryDict(self.raw_post_data), datastructures.MultiValueDict() else: - self._post, self._files = httpwrappers.QueryDict(''), datastructures.MultiValueDict() + self._post, self._files = http.QueryDict(''), datastructures.MultiValueDict() def _get_request(self): if not hasattr(self, '_request'): - self._request = datastructures.MergeDict(self.POST, self.GET) + self._request = datastructures.MergeDict(self.POST, self.GET) return self._request def _get_get(self): if not hasattr(self, '_get'): # The WSGI spec says 'QUERY_STRING' may be absent. - self._get = httpwrappers.QueryDict(self.environ.get('QUERY_STRING', '')) + self._get = http.QueryDict(self.environ.get('QUERY_STRING', '')) return self._get def _set_get(self, get): @@ -98,7 +101,7 @@ class WSGIRequest(httpwrappers.HttpRequest): def _get_cookies(self): if not hasattr(self, '_cookies'): - self._cookies = httpwrappers.parse_cookie(self.environ.get('HTTP_COOKIE', '')) + self._cookies = http.parse_cookie(self.environ.get('HTTP_COOKIE', '')) return self._cookies def _set_cookies(self, cookies): @@ -116,34 +119,16 @@ class WSGIRequest(httpwrappers.HttpRequest): self._raw_post_data = self.environ['wsgi.input'].read(int(self.environ["CONTENT_LENGTH"])) return self._raw_post_data - def _get_user(self): - if not hasattr(self, '_user'): - from django.models.auth import users - try: - user_id = self.session[users.SESSION_KEY] - if not user_id: - raise ValueError - self._user = users.get_object(pk=user_id) - except (AttributeError, KeyError, ValueError, users.UserDoesNotExist): - from django.parts.auth import anonymoususers - self._user = anonymoususers.AnonymousUser() - return self._user - - def _set_user(self, user): - self._user = user - GET = property(_get_get, _set_get) POST = property(_get_post, _set_post) COOKIES = property(_get_cookies, _set_cookies) FILES = property(_get_files) REQUEST = property(_get_request) raw_post_data = property(_get_raw_post_data) - user = property(_get_user, _set_user) class WSGIHandler(BaseHandler): def __call__(self, environ, start_response): from django.conf import settings - from django.core import db if settings.ENABLE_PSYCO: import psyco @@ -154,15 +139,17 @@ class WSGIHandler(BaseHandler): if self._request_middleware is None: self.load_middleware() + dispatcher.send(signal=signals.request_started) try: request = WSGIRequest(environ) response = self.get_response(request.path, request) - finally: - db.db.close() - # Apply response middleware - for middleware_method in self._response_middleware: - response = middleware_method(request, response) + # Apply response middleware + for middleware_method in self._response_middleware: + response = middleware_method(request, response) + + finally: + dispatcher.send(signal=signals.request_finished) try: status_text = STATUS_CODE_TEXT[response.status_code] diff --git a/django/core/mail.py b/django/core/mail.py index 6a3cba141c..3baf191b5c 100644 --- a/django/core/mail.py +++ b/django/core/mail.py @@ -46,9 +46,18 @@ def send_mass_mail(datatuple, fail_silently=False, auth_user=settings.EMAIL_HOST msg['Subject'] = subject msg['From'] = from_email msg['To'] = ', '.join(recipient_list) - server.sendmail(from_email, recipient_list, msg.as_string()) - num_sent += 1 - server.quit() + try: + server.sendmail(from_email, recipient_list, msg.as_string()) + num_sent += 1 + except: + if not fail_silently: + raise + try: + server.quit() + except: + if fail_silently: + return + raise return num_sent def mail_admins(subject, message, fail_silently=False): diff --git a/django/core/management.py b/django/core/management.py index 15f570c57b..3838c2abee 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -2,8 +2,10 @@ # development-server initialization. import django -import os, re, sys, textwrap +from django.core.exceptions import ImproperlyConfigured +import os, re, shutil, sys, textwrap from optparse import OptionParser +from django.utils import termcolors # For Python 2.3 if not hasattr(__builtins__, 'set'): @@ -17,7 +19,7 @@ MODULE_TEMPLATE = ''' {%% if perms.%(app)s.%(addperm)s or perms.%(app)s.%(cha </tr> {%% endif %%}''' -APP_ARGS = '[modelmodule ...]' +APP_ARGS = '[appname ...]' # Use django.__path__[0] because we don't know which directory django into # which has been installed. @@ -25,38 +27,46 @@ PROJECT_TEMPLATE_DIR = os.path.join(django.__path__[0], 'conf', '%s_template') INVALID_PROJECT_NAMES = ('django', 'test') -def _get_packages_insert(app_label): - from django.core.db import db - return "INSERT INTO %s (%s, %s) VALUES ('%s', '%s');" % \ - (db.quote_name('packages'), db.quote_name('label'), db.quote_name('name'), - app_label, app_label) +# Set up the terminal color scheme. +class dummy: pass +style = dummy() +style.ERROR = termcolors.make_style(fg='red', opts=('bold',)) +style.ERROR_OUTPUT = termcolors.make_style(fg='red', opts=('bold',)) +style.SQL_FIELD = termcolors.make_style(fg='green', opts=('bold',)) +style.SQL_COLTYPE = termcolors.make_style(fg='green') +style.SQL_KEYWORD = termcolors.make_style(fg='yellow') +style.SQL_TABLE = termcolors.make_style(opts=('bold',)) +del dummy -def _get_permission_codename(action, opts): - return '%s_%s' % (action, opts.object_name.lower()) +def disable_termcolors(): + class dummy: + def __getattr__(self, attr): + return lambda x: x + global style + style = dummy() -def _get_all_permissions(opts): - "Returns (codename, name) for all permissions in the given opts." - perms = [] - if opts.admin: - for action in ('add', 'change', 'delete'): - perms.append((_get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name))) - return perms + list(opts.permissions) - -def _get_permission_insert(name, codename, opts): - from django.core.db import db - return "INSERT INTO %s (%s, %s, %s) VALUES ('%s', '%s', '%s');" % \ - (db.quote_name('auth_permissions'), db.quote_name('name'), db.quote_name('package'), - db.quote_name('codename'), name.replace("'", "''"), opts.app_label, codename) - -def _get_contenttype_insert(opts): - from django.core.db import db - return "INSERT INTO %s (%s, %s, %s) VALUES ('%s', '%s', '%s');" % \ - (db.quote_name('content_types'), db.quote_name('name'), db.quote_name('package'), - db.quote_name('python_module_name'), opts.verbose_name, opts.app_label, opts.module_name) +# Disable terminal coloring if somebody's piping the output. +if (not sys.stdout.isatty()) or (sys.platform == 'win32'): + disable_termcolors() def _is_valid_dir_name(s): return bool(re.search(r'^\w+$', s)) +def _get_installed_models(table_list): + "Gets a set of all models that are installed, given a list of existing tables" + from django.db import models + all_models = [] + for app in models.get_apps(): + for model in models.get_models(app): + all_models.append(model) + return set([m for m in all_models if m._meta.db_table in table_list]) + +def _get_table_list(): + "Gets a list of all db tables that are physically installed." + from django.db import connection, get_introspection_module + cursor = connection.cursor() + return get_introspection_module().get_table_list(cursor) + # If the foreign key points to an AutoField, a PositiveIntegerField or a # PositiveSmallIntegerField, the foreign key should be an IntegerField, not the # referred field type. Otherwise, the foreign key should be the same type of @@ -67,304 +77,429 @@ def get_version(): "Returns the version as a human-format string." from django import VERSION v = '.'.join([str(i) for i in VERSION[:-1]]) - if VERSION[3]: - v += ' (%s)' % VERSION[3] + if VERSION[-1]: + v += ' (%s)' % VERSION[-1] return v -def get_sql_create(mod): - "Returns a list of the CREATE TABLE SQL statements for the given module." - from django.core import db, meta +def get_sql_create(app): + "Returns a list of the CREATE TABLE SQL statements for the given app." + from django.db import get_creation_module, models + data_types = get_creation_module().DATA_TYPES + + if not data_types: + # This must be the "dummy" database backend, which means the user + # hasn't set DATABASE_ENGINE. + sys.stderr.write(style.ERROR("Error: Django doesn't know which syntax to use for your SQL statements,\n" + + "because you haven't specified the DATABASE_ENGINE setting.\n" + + "Edit your settings file and change DATABASE_ENGINE to something like 'postgresql' or 'mysql'.\n")) + sys.exit(1) + + # Get installed models, so we generate REFERENCES right + installed_models = _get_installed_models(_get_table_list()) + final_output = [] - for klass in mod._MODELS: - opts = klass._meta - table_output = [] - for f in opts.fields: - if isinstance(f, meta.ForeignKey): - rel_field = f.rel.get_related_field() - data_type = get_rel_data_type(rel_field) - else: - rel_field = f - data_type = f.get_internal_type() - col_type = db.DATA_TYPES[data_type] - if col_type is not None: - field_output = [db.db.quote_name(f.column), col_type % rel_field.__dict__] - field_output.append('%sNULL' % (not f.null and 'NOT ' or '')) - if f.unique: - field_output.append('UNIQUE') - if f.primary_key: - field_output.append('PRIMARY KEY') - if f.rel: - field_output.append('REFERENCES %s (%s)' % \ - (db.db.quote_name(f.rel.to.db_table), - db.db.quote_name(f.rel.to.get_field(f.rel.field_name).column))) - table_output.append(' '.join(field_output)) - if opts.order_with_respect_to: - table_output.append('%s %s NULL' % (db.db.quote_name('_order'), db.DATA_TYPES['IntegerField'])) - for field_constraints in opts.unique_together: - table_output.append('UNIQUE (%s)' % \ - ", ".join([db.db.quote_name(opts.get_field(f).column) for f in field_constraints])) + models_output = set(installed_models) + pending_references = {} - full_statement = ['CREATE TABLE %s (' % db.db.quote_name(opts.db_table)] - for i, line in enumerate(table_output): # Combine and add commas. - full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) - full_statement.append(');') - final_output.append('\n'.join(full_statement)) + app_models = models.get_models(app) + + for klass in app_models: + output, references = _get_sql_model_create(klass, models_output) + final_output.extend(output) + pending_references.update(references) + final_output.extend(_get_sql_for_pending_references(klass, pending_references)) + # Keep track of the fact that we've created the table for this model. + models_output.add(klass) + + # Create the many-to-many join tables. + for klass in app_models: + final_output.extend(_get_many_to_many_sql_for_model(klass)) + + # Handle references to tables that are from other apps + # but don't exist physically + not_installed_models = set(pending_references.keys()) + if not_installed_models: + final_output.append('-- The following references should be added but depend on non-existant tables:') + for klass in not_installed_models: + final_output.extend(['-- ' + sql for sql in + _get_sql_for_pending_references(klass, pending_references)]) - for klass in mod._MODELS: - opts = klass._meta - for f in opts.many_to_many: - table_output = ['CREATE TABLE %s (' % db.db.quote_name(f.get_m2m_db_table(opts))] - table_output.append(' %s %s NOT NULL PRIMARY KEY,' % (db.db.quote_name('id'), db.DATA_TYPES['AutoField'])) - table_output.append(' %s %s NOT NULL REFERENCES %s (%s),' % \ - (db.db.quote_name(opts.object_name.lower() + '_id'), - db.DATA_TYPES[get_rel_data_type(opts.pk)] % opts.pk.__dict__, - db.db.quote_name(opts.db_table), - db.db.quote_name(opts.pk.column))) - table_output.append(' %s %s NOT NULL REFERENCES %s (%s),' % \ - (db.db.quote_name(f.rel.to.object_name.lower() + '_id'), - db.DATA_TYPES[get_rel_data_type(f.rel.to.pk)] % f.rel.to.pk.__dict__, - db.db.quote_name(f.rel.to.db_table), - db.db.quote_name(f.rel.to.pk.column))) - table_output.append(' UNIQUE (%s, %s)' % \ - (db.db.quote_name(opts.object_name.lower() + '_id'), - db.db.quote_name(f.rel.to.object_name.lower() + '_id'))) - table_output.append(');') - final_output.append('\n'.join(table_output)) return final_output -get_sql_create.help_doc = "Prints the CREATE TABLE SQL statements for the given model module name(s)." +get_sql_create.help_doc = "Prints the CREATE TABLE SQL statements for the given app name(s)." get_sql_create.args = APP_ARGS -def get_sql_delete(mod): - "Returns a list of the DROP TABLE SQL statements for the given module." - from django.core import db +def _get_sql_model_create(klass, models_already_seen=set()): + """ + Get the SQL required to create a single model. + + Returns list_of_sql, pending_references_dict + """ + from django.db import backend, get_creation_module, models + data_types = get_creation_module().DATA_TYPES + + opts = klass._meta + final_output = [] + table_output = [] + pending_references = {} + for f in opts.fields: + if isinstance(f, models.ForeignKey): + rel_field = f.rel.get_related_field() + data_type = get_rel_data_type(rel_field) + else: + rel_field = f + data_type = f.get_internal_type() + col_type = data_types[data_type] + if col_type is not None: + # Make the definition (e.g. 'foo VARCHAR(30)') for this field. + field_output = [style.SQL_FIELD(backend.quote_name(f.column)), + style.SQL_COLTYPE(col_type % rel_field.__dict__)] + field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or ''))) + if f.unique: + field_output.append(style.SQL_KEYWORD('UNIQUE')) + if f.primary_key: + field_output.append(style.SQL_KEYWORD('PRIMARY KEY')) + if f.rel: + if f.rel.to in models_already_seen: + field_output.append(style.SQL_KEYWORD('REFERENCES') + ' ' + \ + style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)) + ' (' + \ + style.SQL_FIELD(backend.quote_name(f.rel.to._meta.get_field(f.rel.field_name).column)) + ')' + ) + else: + # We haven't yet created the table to which this field + # is related, so save it for later. + pr = pending_references.setdefault(f.rel.to, []).append((klass, f)) + table_output.append(' '.join(field_output)) + if opts.order_with_respect_to: + table_output.append(style.SQL_FIELD(backend.quote_name('_order')) + ' ' + \ + style.SQL_COLTYPE(data_types['IntegerField']) + ' ' + \ + style.SQL_KEYWORD('NULL')) + for field_constraints in opts.unique_together: + table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \ + ", ".join([backend.quote_name(style.SQL_FIELD(opts.get_field(f).column)) for f in field_constraints])) + + full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(backend.quote_name(opts.db_table)) + ' ('] + for i, line in enumerate(table_output): # Combine and add commas. + full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + full_statement.append(');') + final_output.append('\n'.join(full_statement)) + + return final_output, pending_references + +def _get_sql_for_pending_references(klass, pending_references): + """ + Get any ALTER TABLE statements to add constraints after the fact. + """ + from django.db import backend, get_creation_module + data_types = get_creation_module().DATA_TYPES + + final_output = [] + if backend.supports_constraints: + opts = klass._meta + if klass in pending_references: + for rel_class, f in pending_references[klass]: + rel_opts = rel_class._meta + r_table = rel_opts.db_table + r_col = f.column + table = opts.db_table + col = opts.get_field(f.rel.field_name).column + final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s);' % \ + (backend.quote_name(r_table), + backend.quote_name('%s_referencing_%s_%s' % (r_col, table, col)), + backend.quote_name(r_col), backend.quote_name(table), backend.quote_name(col))) + del pending_references[klass] + return final_output + +def _get_many_to_many_sql_for_model(klass): + from django.db import backend, get_creation_module + data_types = get_creation_module().DATA_TYPES + + opts = klass._meta + final_output = [] + for f in opts.many_to_many: + table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \ + style.SQL_TABLE(backend.quote_name(f.m2m_db_table())) + ' ('] + table_output.append(' %s %s %s,' % \ + (style.SQL_FIELD(backend.quote_name('id')), + style.SQL_COLTYPE(data_types['AutoField']), + style.SQL_KEYWORD('NOT NULL PRIMARY KEY'))) + table_output.append(' %s %s %s %s (%s),' % \ + (style.SQL_FIELD(backend.quote_name(f.m2m_column_name())), + style.SQL_COLTYPE(data_types[get_rel_data_type(opts.pk)] % opts.pk.__dict__), + style.SQL_KEYWORD('NOT NULL REFERENCES'), + style.SQL_TABLE(backend.quote_name(opts.db_table)), + style.SQL_FIELD(backend.quote_name(opts.pk.column)))) + table_output.append(' %s %s %s %s (%s),' % \ + (style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())), + style.SQL_COLTYPE(data_types[get_rel_data_type(f.rel.to._meta.pk)] % f.rel.to._meta.pk.__dict__), + style.SQL_KEYWORD('NOT NULL REFERENCES'), + style.SQL_TABLE(backend.quote_name(f.rel.to._meta.db_table)), + style.SQL_FIELD(backend.quote_name(f.rel.to._meta.pk.column)))) + table_output.append(' %s (%s, %s)' % \ + (style.SQL_KEYWORD('UNIQUE'), + style.SQL_FIELD(backend.quote_name(f.m2m_column_name())), + style.SQL_FIELD(backend.quote_name(f.m2m_reverse_name())))) + table_output.append(');') + final_output.append('\n'.join(table_output)) + return final_output + +def get_sql_delete(app): + "Returns a list of the DROP TABLE SQL statements for the given app." + from django.db import backend, connection, models, get_introspection_module + introspection = get_introspection_module() + + # This should work even if a connecton isn't available try: - cursor = db.db.cursor() + cursor = connection.cursor() except: cursor = None - # Determine whether the admin log table exists. It only exists if the - # person has installed the admin app. - try: - if cursor is not None: - # Check whether the table exists. - cursor.execute("SELECT 1 FROM %s LIMIT 1" % db.db.quote_name('django_admin_log')) - except: - # The table doesn't exist, so it doesn't need to be dropped. - db.db.rollback() - admin_log_exists = False + # Figure out which tables already exist + if cursor: + table_names = introspection.get_table_list(cursor) else: - admin_log_exists = True + table_names = [] output = [] # Output DROP TABLE statements for standard application tables. - for klass in mod._MODELS: - try: - if cursor is not None: - # Check whether the table exists. - cursor.execute("SELECT 1 FROM %s LIMIT 1" % db.db.quote_name(klass._meta.db_table)) - except: - # The table doesn't exist, so it doesn't need to be dropped. - db.db.rollback() - else: - output.append("DROP TABLE %s;" % db.db.quote_name(klass._meta.db_table)) + to_delete = set() + + references_to_delete = {} + app_models = models.get_models(app) + for klass in app_models: + if cursor and klass._meta.db_table in table_names: + # The table exists, so it needs to be dropped + opts = klass._meta + for f in opts.fields: + if f.rel and f.rel.to not in to_delete: + references_to_delete.setdefault(f.rel.to, []).append( (klass, f) ) + + to_delete.add(klass) + + for klass in app_models: + if cursor and klass._meta.db_table in table_names: + # Drop the table now + output.append('%s %s;' % (style.SQL_KEYWORD('DROP TABLE'), + style.SQL_TABLE(backend.quote_name(klass._meta.db_table)))) + if backend.supports_constraints and references_to_delete.has_key(klass): + for rel_class, f in references_to_delete[klass]: + table = rel_class._meta.db_table + col = f.column + r_table = klass._meta.db_table + r_col = klass._meta.get_field(f.rel.field_name).column + output.append('%s %s %s %s;' % \ + (style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_TABLE(backend.quote_name(table)), + style.SQL_KEYWORD(backend.get_drop_foreignkey_sql()), + style.SQL_FIELD(backend.quote_name("%s_referencing_%s_%s" % (col, r_table, r_col))))) + del references_to_delete[klass] # Output DROP TABLE statements for many-to-many tables. - for klass in mod._MODELS: + for klass in app_models: opts = klass._meta for f in opts.many_to_many: - try: - if cursor is not None: - cursor.execute("SELECT 1 FROM %s LIMIT 1" % db.db.quote_name(f.get_m2m_db_table(opts))) - except: - db.db.rollback() - else: - output.append("DROP TABLE %s;" % db.db.quote_name(f.get_m2m_db_table(opts))) + if cursor and f.m2m_db_table() in table_names: + output.append("%s %s;" % (style.SQL_KEYWORD('DROP TABLE'), + style.SQL_TABLE(backend.quote_name(f.m2m_db_table())))) - app_label = mod._MODELS[0]._meta.app_label - - # Delete from packages, auth_permissions, content_types. - output.append("DELETE FROM %s WHERE %s = '%s';" % \ - (db.db.quote_name('packages'), db.db.quote_name('label'), app_label)) - output.append("DELETE FROM %s WHERE %s = '%s';" % \ - (db.db.quote_name('auth_permissions'), db.db.quote_name('package'), app_label)) - output.append("DELETE FROM %s WHERE %s = '%s';" % \ - (db.db.quote_name('content_types'), db.db.quote_name('package'), app_label)) - - # Delete from the admin log. - if cursor is not None: - cursor.execute("SELECT %s FROM %s WHERE %s = %%s" % \ - (db.db.quote_name('id'), db.db.quote_name('content_types'), - db.db.quote_name('package')), [app_label]) - if admin_log_exists: - for row in cursor.fetchall(): - output.append("DELETE FROM %s WHERE %s = %s;" % \ - (db.db.quote_name('django_admin_log'), db.db.quote_name('content_type_id'), row[0])) + app_label = app_models[0]._meta.app_label # Close database connection explicitly, in case this output is being piped # directly into a database client, to avoid locking issues. - if cursor is not None: + if cursor: cursor.close() - db.db.close() + connection.close() return output[::-1] # Reverse it, to deal with table dependencies. -get_sql_delete.help_doc = "Prints the DROP TABLE SQL statements for the given model module name(s)." +get_sql_delete.help_doc = "Prints the DROP TABLE SQL statements for the given app name(s)." get_sql_delete.args = APP_ARGS -def get_sql_reset(mod): +def get_sql_reset(app): "Returns a list of the DROP TABLE SQL, then the CREATE TABLE SQL, for the given module." - return get_sql_delete(mod) + get_sql_all(mod) -get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given model module name(s)." + return get_sql_delete(app) + get_sql_all(app) +get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given app name(s)." get_sql_reset.args = APP_ARGS -def get_sql_initial_data(mod): - "Returns a list of the initial INSERT SQL statements for the given module." - from django.core import db +def get_sql_initial_data_for_model(model): + from django.db import models + from django.conf import settings + + opts = model._meta + app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) output = [] - app_label = mod._MODELS[0]._meta.app_label - output.append(_get_packages_insert(app_label)) - app_dir = os.path.normpath(os.path.join(os.path.dirname(mod.__file__), '..', 'sql')) - for klass in mod._MODELS: - opts = klass._meta - # Add custom SQL, if it's available. - sql_files = [os.path.join(app_dir, opts.module_name + '.' + db.DATABASE_ENGINE + '.sql'), - os.path.join(app_dir, opts.module_name + '.sql')] - for sql_file in sql_files: - if os.path.exists(sql_file): - fp = open(sql_file) - output.append(fp.read()) - fp.close() + # Find custom SQL, if it's available. + sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.object_name.lower(), settings.DATABASE_ENGINE)), + os.path.join(app_dir, "%s.sql" % opts.object_name.lower())] + for sql_file in sql_files: + if os.path.exists(sql_file): + fp = open(sql_file) + output.append(fp.read()) + fp.close() - # Content types. - output.append(_get_contenttype_insert(opts)) - # Permissions. - for codename, name in _get_all_permissions(opts): - output.append(_get_permission_insert(name, codename, opts)) return output -get_sql_initial_data.help_doc = "Prints the initial INSERT SQL statements for the given model module name(s)." + +def get_sql_initial_data(app): + "Returns a list of the initial INSERT SQL statements for the given app." + from django.db.models import get_models + output = [] + + app_models = get_models(app) + app_dir = os.path.normpath(os.path.join(os.path.dirname(app.__file__), 'sql')) + + for klass in app_models: + output.extend(get_sql_initial_data_for_model(klass)) + + return output +get_sql_initial_data.help_doc = "Prints the initial INSERT SQL statements for the given app name(s)." get_sql_initial_data.args = APP_ARGS -def get_sql_sequence_reset(mod): - "Returns a list of the SQL statements to reset PostgreSQL sequences for the given module." - from django.core import db, meta +def get_sql_sequence_reset(app): + "Returns a list of the SQL statements to reset PostgreSQL sequences for the given app." + from django.db import backend, models output = [] - for klass in mod._MODELS: + for klass in models.get_models(app): for f in klass._meta.fields: - if isinstance(f, meta.AutoField): - output.append("SELECT setval('%s_%s_seq', (SELECT max(%s) FROM %s));" % \ - (klass._meta.db_table, f.column, db.db.quote_name(f.column), - db.db.quote_name(klass._meta.db_table))) + if isinstance(f, models.AutoField): + output.append("%s setval('%s', (%s max(%s) %s %s));" % \ + (style.SQL_KEYWORD('SELECT'), + style.SQL_FIELD('%s_%s_seq' % (klass._meta.db_table, f.column)), + style.SQL_KEYWORD('SELECT'), + style.SQL_FIELD(backend.quote_name(f.column)), + style.SQL_KEYWORD('FROM'), + style.SQL_TABLE(backend.quote_name(klass._meta.db_table)))) + break # Only one AutoField is allowed per model, so don't bother continuing. for f in klass._meta.many_to_many: - output.append("SELECT setval('%s_id_seq', (SELECT max(%s) FROM %s));" % \ - (f.get_m2m_db_table(klass._meta), db.db.quote_name('id'), f.get_m2m_db_table(klass._meta))) + output.append("%s setval('%s', (%s max(%s) %s %s));" % \ + (style.SQL_KEYWORD('SELECT'), + style.SQL_FIELD('%s_id_seq' % f.m2m_db_table()), + style.SQL_KEYWORD('SELECT'), + style.SQL_FIELD(backend.quote_name('id')), + style.SQL_KEYWORD('FROM'), + style.SQL_TABLE(f.m2m_db_table()))) return output -get_sql_sequence_reset.help_doc = "Prints the SQL statements for resetting PostgreSQL sequences for the given model module name(s)." +get_sql_sequence_reset.help_doc = "Prints the SQL statements for resetting PostgreSQL sequences for the given app name(s)." get_sql_sequence_reset.args = APP_ARGS -def get_sql_indexes(mod): - "Returns a list of the CREATE INDEX SQL statements for the given module." - from django.core.db import db +def get_sql_indexes(app): + "Returns a list of the CREATE INDEX SQL statements for the given app." + from django.db import backend, models output = [] - for klass in mod._MODELS: + + for klass in models.get_models(app): for f in klass._meta.fields: if f.db_index: - unique = f.unique and "UNIQUE " or "" - output.append("CREATE %sINDEX %s_%s ON %s (%s);" % \ - (unique, klass._meta.db_table, f.column, - db.quote_name(klass._meta.db_table), db.quote_name(f.column))) + unique = f.unique and 'UNIQUE ' or '' + output.append( + style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \ + style.SQL_TABLE('%s_%s' % (klass._meta.db_table, f.column)) + ' ' + \ + style.SQL_KEYWORD('ON') + ' ' + \ + style.SQL_TABLE(backend.quote_name(klass._meta.db_table)) + ' ' + \ + "(%s);" % style.SQL_FIELD(backend.quote_name(f.column)) + ) return output get_sql_indexes.help_doc = "Prints the CREATE INDEX SQL statements for the given model module name(s)." get_sql_indexes.args = APP_ARGS -def get_sql_all(mod): - "Returns a list of CREATE TABLE SQL and initial-data insert for the given module." - return get_sql_create(mod) + get_sql_initial_data(mod) -get_sql_all.help_doc = "Prints the CREATE TABLE and initial-data SQL statements for the given model module name(s)." +def get_sql_all(app): + "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module." + return get_sql_create(app) + get_sql_initial_data(app) + get_sql_indexes(app) +get_sql_all.help_doc = "Prints the CREATE TABLE, initial-data and CREATE INDEX SQL statements for the given model module name(s)." get_sql_all.args = APP_ARGS -def has_no_records(cursor): - "Returns True if the cursor, having executed a query, returned no records." - # This is necessary due to an inconsistency in the DB-API spec. - # cursor.rowcount can be -1 (undetermined), according to - # http://www.python.org/peps/pep-0249.html - if cursor.rowcount < 0: - return cursor.fetchone() is None - return cursor.rowcount < 1 - -def database_check(mod): - "Checks that everything is properly installed in the database for the given module." - from django.core import db - cursor = db.db.cursor() - app_label = mod._MODELS[0]._meta.app_label +def syncdb(): + "Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created." + from django.db import connection, transaction, models, get_creation_module + from django.db.models import signals + from django.conf import settings + from django.dispatch import dispatcher - # Check that the package exists in the database. - cursor.execute("SELECT 1 FROM %s WHERE %s = %%s" % \ - (db.db.quote_name('packages'), db.db.quote_name('label')), [app_label]) - if has_no_records(cursor): -# sys.stderr.write("The '%s' package isn't installed.\n" % app_label) - print _get_packages_insert(app_label) + disable_termcolors() - # Check that the permissions and content types are in the database. - perms_seen = {} - contenttypes_seen = {} - for klass in mod._MODELS: - opts = klass._meta - perms = _get_all_permissions(opts) - perms_seen.update(dict(perms)) - contenttypes_seen[opts.module_name] = 1 - for codename, name in perms: - cursor.execute("SELECT 1 FROM %s WHERE %s = %%s AND %s = %%s" % \ - (db.db.quote_name('auth_permissions'), db.db.quote_name('package'), - db.db.quote_name('codename')), (app_label, codename)) - if has_no_records(cursor): -# sys.stderr.write("The '%s.%s' permission doesn't exist.\n" % (app_label, codename)) - print _get_permission_insert(name, codename, opts) - cursor.execute("SELECT 1 FROM %s WHERE %s = %%s AND %s = %%s" % \ - (db.db.quote_name('content_types'), db.db.quote_name('package'), - db.db.quote_name('python_module_name')), (app_label, opts.module_name)) - if has_no_records(cursor): -# sys.stderr.write("The '%s.%s' content type doesn't exist.\n" % (app_label, opts.module_name)) - print _get_contenttype_insert(opts) + # First, try validating the models. + _check_for_validation_errors() - # Check that there aren't any *extra* permissions in the DB that the model - # doesn't know about. - cursor.execute("SELECT %s FROM %s WHERE %s = %%s" % \ - (db.db.quote_name('codename'), db.db.quote_name('auth_permissions'), - db.db.quote_name('package')), (app_label,)) - for row in cursor.fetchall(): + # Import the 'management' module within each installed app, to register + # dispatcher events. + for app_name in settings.INSTALLED_APPS: try: - perms_seen[row[0]] - except KeyError: -# sys.stderr.write("A permission called '%s.%s' was found in the database but not in the model.\n" % (app_label, row[0])) - print "DELETE FROM %s WHERE %s='%s' AND %s = '%s';" % \ - (db.db.quote_name('auth_permissions'), db.db.quote_name('package'), - app_label, db.db.quote_name('codename'), row[0]) + __import__(app_name + '.management', '', '', ['']) + except ImportError: + pass - # Check that there aren't any *extra* content types in the DB that the - # model doesn't know about. - cursor.execute("SELECT %s FROM %s WHERE %s = %%s" % \ - (db.db.quote_name('python_module_name'), db.db.quote_name('content_types'), - db.db.quote_name('package')), (app_label,)) - for row in cursor.fetchall(): - try: - contenttypes_seen[row[0]] - except KeyError: -# sys.stderr.write("A content type called '%s.%s' was found in the database but not in the model.\n" % (app_label, row[0])) - print "DELETE FROM %s WHERE %s='%s' AND %s = '%s';" % \ - (db.db.quote_name('content_types'), db.db.quote_name('package'), - app_label, db.db.quote_name('python_module_name'), row[0]) -database_check.help_doc = "Checks that everything is installed in the database for the given model module name(s) and prints SQL statements if needed." -database_check.args = APP_ARGS + data_types = get_creation_module().DATA_TYPES + + cursor = connection.cursor() + + # Get a list of all existing database tables, + # so we know what needs to be added. + table_list = _get_table_list() + + # Get a list of already installed *models* so that references work right. + seen_models = _get_installed_models(table_list) + created_models = set() + pending_references = {} + + for app in models.get_apps(): + model_list = models.get_models(app) + for model in model_list: + # Create the model's database table, if it doesn't already exist. + if model._meta.db_table in table_list: + continue + sql, references = _get_sql_model_create(model, seen_models) + seen_models.add(model) + created_models.add(model) + pending_references.update(references) + sql.extend(_get_sql_for_pending_references(model, pending_references)) + print "Creating table %s" % model._meta.db_table + for statement in sql: + cursor.execute(statement) + + for model in model_list: + if model in created_models: + sql = _get_many_to_many_sql_for_model(model) + if sql: + print "Creating many-to-many tables for %s model" % model.__name__ + for statement in sql: + cursor.execute(statement) + + transaction.commit_unless_managed() + + # Send the post_syncdb signal, so individual apps can do whatever they need + # to do at this point. + for app in models.get_apps(): + dispatcher.send(signal=signals.post_syncdb, sender=app, + app=app, created_models=created_models) + + # Install initial data for the app (but only if this is a model we've + # just created) + for model in models.get_models(app): + if model in created_models: + initial_sql = get_sql_initial_data_for_model(model) + if initial_sql: + print "Installing initial data for %s model" % model._meta.object_name + try: + for sql in initial_sql: + cursor.execute(sql) + except Exception, e: + sys.stderr.write("Failed to install initial SQL data for %s model: %s" % \ + (model._meta.object_name, e)) + transaction.rollback_unless_managed() + else: + transaction.commit_unless_managed() + +syncdb.args = '' -def get_admin_index(mod): - "Returns admin-index template snippet (in list form) for the given module." +def get_admin_index(app): + "Returns admin-index template snippet (in list form) for the given app." from django.utils.text import capfirst + from django.db.models import get_models output = [] - app_label = mod._MODELS[0]._meta.app_label + app_models = get_models(app) + app_label = app_models[0]._meta.app_label output.append('{%% if perms.%s %%}' % app_label) output.append('<div class="module"><h2>%s</h2><table>' % app_label.title()) - for klass in mod._MODELS: + for klass in app_models: if klass._meta.admin: output.append(MODULE_TEMPLATE % { 'app': app_label, @@ -376,97 +511,114 @@ def get_admin_index(mod): output.append('</table></div>') output.append('{% endif %}') return output -get_admin_index.help_doc = "Prints the admin-index template snippet for the given model module name(s)." +get_admin_index.help_doc = "Prints the admin-index template snippet for the given app name(s)." get_admin_index.args = APP_ARGS -def init(): - "Initializes the database with auth and core." - try: - from django.core import db, meta - auth = meta.get_app('auth') - core = meta.get_app('core') - cursor = db.db.cursor() - for sql in get_sql_create(core) + get_sql_create(auth) + get_sql_initial_data(core) + get_sql_initial_data(auth): - cursor.execute(sql) - cursor.execute("INSERT INTO %s (%s, %s) VALUES ('example.com', 'Example site')" % \ - (db.db.quote_name(core.Site._meta.db_table), db.db.quote_name('domain'), - db.db.quote_name('name'))) - except Exception, e: - sys.stderr.write("Error: The database couldn't be initialized.\n%s\n" % e) - try: - db.db.rollback() - except UnboundLocalError: - pass - sys.exit(1) - else: - db.db.commit() -init.args = '' +def _module_to_dict(module, omittable=lambda k: k.startswith('_')): + "Converts a module namespace to a Python dictionary. Used by get_settings_diff." + return dict([(k, repr(v)) for k, v in module.__dict__.items() if not omittable(k)]) + +def diffsettings(): + """ + Displays differences between the current settings.py and Django's + default settings. Settings that don't appear in the defaults are + followed by "###". + """ + # Inspired by Postfix's "postconf -n". + from django.conf import settings, global_settings + + user_settings = _module_to_dict(settings) + default_settings = _module_to_dict(global_settings) -def install(mod): + output = [] + keys = user_settings.keys() + keys.sort() + for key in keys: + if key not in default_settings: + output.append("%s = %s ###" % (key, user_settings[key])) + elif user_settings[key] != default_settings[key]: + output.append("%s = %s" % (key, user_settings[key])) + print '\n'.join(output) +diffsettings.args = "" + +def install(app): "Executes the equivalent of 'get_sql_all' in the current database." - from django.core import db - from cStringIO import StringIO - mod_name = mod.__name__[mod.__name__.rindex('.')+1:] + from django.db import connection, transaction + + app_name = app.__name__.split('.')[-2] + + disable_termcolors() # First, try validating the models. - s = StringIO() - num_errors = get_validation_errors(s) - if num_errors: - sys.stderr.write("Error: %s couldn't be installed, because there were errors in your model:\n" % mod_name) - s.seek(0) - sys.stderr.write(s.read()) - sys.exit(1) - sql_list = get_sql_all(mod) + _check_for_validation_errors(app) + + sql_list = get_sql_all(app) try: - cursor = db.db.cursor() + cursor = connection.cursor() for sql in sql_list: cursor.execute(sql) except Exception, e: - sys.stderr.write("""Error: %s couldn't be installed. Possible reasons: + sys.stderr.write(style.ERROR("""Error: %s couldn't be installed. Possible reasons: * The database isn't running or isn't configured correctly. * At least one of the database tables already exists. * The SQL was invalid. Hint: Look at the output of 'django-admin.py sqlall %s'. That's the SQL this command wasn't able to run. -The full error: %s\n""" % \ - (mod_name, mod_name, e)) - db.db.rollback() +The full error: """ % (app_name, app_name)) + style.ERROR_OUTPUT(str(e)) + '\n') + transaction.rollback_unless_managed() sys.exit(1) - db.db.commit() -install.help_doc = "Executes ``sqlall`` for the given model module name(s) in the current database." + transaction.commit_unless_managed() +install.help_doc = "Executes ``sqlall`` for the given app(s) in the current database." install.args = APP_ARGS -def installperms(mod): - "Installs any permissions for the given model, if needed." - from django.models.auth import permissions - from django.models.core import packages - num_added = 0 - package = packages.get_object(pk=mod._MODELS[0]._meta.app_label) - for klass in mod._MODELS: - opts = klass._meta - for codename, name in _get_all_permissions(opts): - try: - permissions.get_object(name__exact=name, codename__exact=codename, package__label__exact=package.label) - except permissions.PermissionDoesNotExist: - p = permissions.Permission(name=name, package=package, codename=codename) - p.save() - print "Added permission '%r'." % p - num_added += 1 - if not num_added: - print "No permissions were added, because all necessary permissions were already installed." -installperms.help_doc = "Installs any permissions for the given model module name(s), if needed." -installperms.args = APP_ARGS +def reset(app): + "Executes the equivalent of 'get_sql_reset' in the current database." + from django.db import connection, transaction + from cStringIO import StringIO + app_name = app.__name__.split('.')[-2] + + disable_termcolors() + + # First, try validating the models. + _check_for_validation_errors(app) + sql_list = get_sql_reset(app) + + confirm = raw_input(""" +You have requested a database reset. +This will IRREVERSIBLY DESTROY any data in your database. +Are you sure you want to do this? + +Type 'yes' to continue, or 'no' to cancel: """) + if confirm == 'yes': + try: + cursor = connection.cursor() + for sql in sql_list: + cursor.execute(sql) + except Exception, e: + sys.stderr.write(style.ERROR("""Error: %s couldn't be installed. Possible reasons: + * The database isn't running or isn't configured correctly. + * At least one of the database tables already exists. + * The SQL was invalid. +Hint: Look at the output of 'django-admin.py sqlreset %s'. That's the SQL this command wasn't able to run. +The full error: """ % (app_name, app_name)) + style.ERROR_OUTPUT(str(e)) + '\n') + transaction.rollback_unless_managed() + sys.exit(1) + transaction.commit_unless_managed() + else: + print "Reset cancelled." +reset.help_doc = "Executes ``sqlreset`` for the given app(s) in the current database." +reset.args = APP_ARGS def _start_helper(app_or_project, name, directory, other_name=''): other = {'project': 'app', 'app': 'project'}[app_or_project] if not _is_valid_dir_name(name): - sys.stderr.write("Error: %r is not a valid %s name. Please use only numbers, letters and underscores.\n" % (name, app_or_project)) + sys.stderr.write(style.ERROR("Error: %r is not a valid %s name. Please use only numbers, letters and underscores.\n" % (name, app_or_project))) sys.exit(1) top_dir = os.path.join(directory, name) try: os.mkdir(top_dir) except OSError, e: - sys.stderr.write("Error: %s\n" % e) + sys.stderr.write(style.ERROR("Error: %s\n" % e)) sys.exit(1) template_dir = PROJECT_TEMPLATE_DIR % app_or_project for d, subdirs, files in os.walk(template_dir): @@ -479,17 +631,20 @@ def _start_helper(app_or_project, name, directory, other_name=''): for f in files: if f.endswith('.pyc'): continue - fp_old = open(os.path.join(d, f), 'r') - fp_new = open(os.path.join(top_dir, relative_dir, f.replace('%s_name' % app_or_project, name)), 'w') + path_old = os.path.join(d, f) + path_new = os.path.join(top_dir, relative_dir, f.replace('%s_name' % app_or_project, name)) + fp_old = open(path_old, 'r') + fp_new = open(path_new, 'w') fp_new.write(fp_old.read().replace('{{ %s_name }}' % app_or_project, name).replace('{{ %s_name }}' % other, other_name)) fp_old.close() fp_new.close() + shutil.copymode(path_old, path_new) def startproject(project_name, directory): "Creates a Django project for the given project_name in the given directory." from random import choice if project_name in INVALID_PROJECT_NAMES: - sys.stderr.write("Error: %r isn't a valid project name. Please try another.\n" % project_name) + sys.stderr.write(style.ERROR("Error: %r isn't a valid project name. Please try another.\n" % project_name)) sys.exit(1) _start_helper('project', project_name, directory) # Create a random SECRET_KEY hash, and put it in the main settings. @@ -513,71 +668,19 @@ def startapp(app_name, directory): startapp.help_doc = "Creates a Django app directory structure for the given app name in the current directory." startapp.args = "[appname]" -def createsuperuser(username=None, email=None, password=None): - "Creates a superuser account." - from django.core import validators - from django.models.auth import users - import getpass - try: - while 1: - if not username: - username = raw_input('Username (only letters, digits and underscores): ') - if not username.isalnum(): - sys.stderr.write("Error: That username is invalid.\n") - username = None - try: - users.get_object(username__exact=username) - except users.UserDoesNotExist: - break - else: - sys.stderr.write("Error: That username is already taken.\n") - username = None - while 1: - if not email: - email = raw_input('E-mail address: ') - try: - validators.isValidEmail(email, None) - except validators.ValidationError: - sys.stderr.write("Error: That e-mail address is invalid.\n") - email = None - else: - break - while 1: - if not password: - password = getpass.getpass() - password2 = getpass.getpass('Password (again): ') - if password != password2: - sys.stderr.write("Error: Your passwords didn't match.\n") - password = None - continue - if password.strip() == '': - sys.stderr.write("Error: Blank passwords aren't allowed.\n") - password = None - continue - break - except KeyboardInterrupt: - sys.stderr.write("\nOperation cancelled.\n") - sys.exit(1) - u = users.create_user(username, email, password) - u.is_staff = True - u.is_active = True - u.is_superuser = True - u.save() - print "User created successfully." -createsuperuser.args = '[username] [email] [password] (Either all or none)' - -def inspectdb(db_name): +def inspectdb(): "Generator that introspects the tables in the given database name and returns a Django model, one line at a time." - from django.core import db + from django.db import connection, get_introspection_module from django.conf import settings import keyword + introspection_module = get_introspection_module() + def table2model(table_name): object_name = table_name.title().replace('_', '') return object_name.endswith('s') and object_name[:-1] or object_name - settings.DATABASE_NAME = db_name - cursor = db.db.cursor() + cursor = connection.cursor() yield "# This is an auto-generated Django model module." yield "# You'll have to do the following manually to clean this up:" yield "# * Rearrange models' order" @@ -587,19 +690,19 @@ def inspectdb(db_name): yield "# Also note: You'll have to insert the output of 'django-admin.py sqlinitialdata [appname]'" yield "# into your database." yield '' - yield 'from django.core import meta' + yield 'from django.db import models' yield '' - for table_name in db.get_table_list(cursor): - yield 'class %s(meta.Model):' % table2model(table_name) + for table_name in introspection_module.get_table_list(cursor): + yield 'class %s(models.Model):' % table2model(table_name) try: - relations = db.get_relations(cursor, table_name) + relations = introspection_module.get_relations(cursor, table_name) except NotImplementedError: relations = {} try: - indexes = db.get_indexes(cursor, table_name) + indexes = introspection_module.get_indexes(cursor, table_name) except NotImplementedError: indexes = {} - for i, row in enumerate(db.get_table_description(cursor, table_name)): + for i, row in enumerate(introspection_module.get_table_description(cursor, table_name)): att_name = row[0] comment_notes = [] # Holds Field notes, to be displayed in a Python comment. extra_params = {} # Holds Field parameters such as 'db_column'. @@ -618,7 +721,7 @@ def inspectdb(db_name): extra_params['db_column'] = att_name else: try: - field_type = db.DATA_TYPES_REVERSE[row[1]] + field_type = introspection_module.DATA_TYPES_REVERSE[row[1]] except KeyError: field_type = 'TextField' comment_notes.append('This field type is a guess.') @@ -652,7 +755,14 @@ def inspectdb(db_name): if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}: continue - field_desc = '%s = meta.%s' % (att_name, field_type) + # Add 'null' and 'blank', if the 'null_ok' flag was present in the + # table description. + if row[6]: # If it's NULL... + extra_params['blank'] = True + if not field_type in ('TextField', 'CharField'): + extra_params['null'] = True + + field_desc = '%s = models.%s' % (att_name, field_type) if extra_params: if not field_desc.endswith('('): field_desc += ', ' @@ -661,11 +771,11 @@ def inspectdb(db_name): if comment_notes: field_desc += ' # ' + ' '.join(comment_notes) yield ' %s' % field_desc - yield ' class META:' + yield ' class Meta:' yield ' db_table = %r' % table_name yield '' inspectdb.help_doc = "Introspects the database tables in the given database and outputs a Django model module." -inspectdb.args = "[dbname]" +inspectdb.args = "" class ModelErrorCollection: def __init__(self, outfile=sys.stdout): @@ -674,123 +784,182 @@ class ModelErrorCollection: def add(self, opts, error): self.errors.append((opts, error)) - self.outfile.write("%s.%s: %s\n" % (opts.app_label, opts.module_name, error)) + self.outfile.write(style.ERROR("%s.%s: %s\n" % (opts.app_label, opts.module_name, error))) + +def get_validation_errors(outfile, app=None): + """ + Validates all models that are part of the specified app. If no app name is provided, + validates all models of all installed apps. Writes errors, if any, to outfile. + Returns number of errors. + """ + from django.db import models + from django.db.models.fields.related import RelatedObject -def get_validation_errors(outfile): - "Validates all installed models. Writes errors, if any, to outfile. Returns number of errors." - import django.models - from django.core import meta e = ModelErrorCollection(outfile) - module_list = meta.get_installed_model_modules() - for module in module_list: - for mod in module._MODELS: - opts = mod._meta + for cls in models.get_models(app): + opts = cls._meta - # Do field-specific validation. - for f in opts.fields: - if isinstance(f, meta.CharField) and f.maxlength in (None, 0): - e.add(opts, '"%s" field: CharFields require a "maxlength" attribute.' % f.name) - if isinstance(f, meta.FloatField): - if f.decimal_places is None: - e.add(opts, '"%s" field: FloatFields require a "decimal_places" attribute.' % f.name) - if f.max_digits is None: - e.add(opts, '"%s" field: FloatFields require a "max_digits" attribute.' % f.name) - if isinstance(f, meta.FileField) and not f.upload_to: - e.add(opts, '"%s" field: FileFields require an "upload_to" attribute.' % f.name) - if isinstance(f, meta.ImageField): - try: - from PIL import Image - except ImportError: - e.add(opts, '"%s" field: To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name) - if f.prepopulate_from is not None and type(f.prepopulate_from) not in (list, tuple): - e.add(opts, '"%s" field: prepopulate_from should be a list or tuple.' % f.name) - if f.choices: - if not type(f.choices) in (tuple, list): - e.add(opts, '"%s" field: "choices" should be either a tuple or list.' % f.name) - else: - for c in f.choices: - if not type(c) in (tuple, list) or len(c) != 2: - e.add(opts, '"%s" field: "choices" should be a sequence of two-tuples.' % f.name) - if f.db_index not in (None, True, False): - e.add(opts, '"%s" field: "db_index" should be either None, True or False.' % f.name) + # Do field-specific validation. + for f in opts.fields: + # Check for deprecated args + dep_args = getattr(f, 'deprecated_args', None) + if dep_args: + e.add(opts, "'%s' Initialized with deprecated args:%s" % (f.name, ",".join(dep_args))) + if isinstance(f, models.CharField) and f.maxlength in (None, 0): + e.add(opts, '"%s": CharFields require a "maxlength" attribute.' % f.name) + if isinstance(f, models.FloatField): + if f.decimal_places is None: + e.add(opts, '"%s": FloatFields require a "decimal_places" attribute.' % f.name) + if f.max_digits is None: + e.add(opts, '"%s": FloatFields require a "max_digits" attribute.' % f.name) + if isinstance(f, models.FileField) and not f.upload_to: + e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name) + if isinstance(f, models.ImageField): + try: + from PIL import Image + except ImportError: + e.add(opts, '"%s": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name) + if f.prepopulate_from is not None and type(f.prepopulate_from) not in (list, tuple): + e.add(opts, '"%s": prepopulate_from should be a list or tuple.' % f.name) + if f.choices: + if not type(f.choices) in (tuple, list): + e.add(opts, '"%s": "choices" should be either a tuple or list.' % f.name) + else: + for c in f.choices: + if not type(c) in (tuple, list) or len(c) != 2: + e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name) + if f.db_index not in (None, True, False): + e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name) + + # Check to see if the related field will clash with any + # existing fields, m2m fields, m2m related objects or related objects + if f.rel: + rel_opts = f.rel.to._meta + if f.rel.to not in models.get_models(): + e.add(opts, "'%s' has relation with uninstalled model %s" % (f.name, rel_opts.object_name)) + + rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() + for r in rel_opts.fields: + if r.name == rel_name: + e.add(opts, "'%s' accessor name '%s.%s' clashes with another field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.many_to_many: + if r.name == rel_name: + e.add(opts, "'%s' accessor name '%s.%s' clashes with a m2m field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.get_all_related_many_to_many_objects(): + if r.get_accessor_name() == rel_name: + e.add(opts, "'%s' accessor name '%s.%s' clashes with a related m2m field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + for r in rel_opts.get_all_related_objects(): + if r.get_accessor_name() == rel_name and r.field is not f: + e.add(opts, "'%s' accessor name '%s.%s' clashes with another related field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + + for i, f in enumerate(opts.many_to_many): + # Check to see if the related m2m field will clash with any + # existing fields, m2m fields, m2m related objects or related objects + rel_opts = f.rel.to._meta + if f.rel.to not in models.get_models(): + e.add(opts, "'%s' has m2m relation with uninstalled model %s" % (f.name, rel_opts.object_name)) - # Check for multiple ManyToManyFields to the same object, and - # verify "singular" is set in that case. - for i, f in enumerate(opts.many_to_many): - for previous_f in opts.many_to_many[:i]: - if f.rel.to == previous_f.rel.to and f.rel.singular == previous_f.rel.singular: - e.add(opts, 'The "%s" field requires a "singular" parameter, because the %s model has more than one ManyToManyField to the same model (%s).' % (f.name, opts.object_name, previous_f.rel.to.object_name)) + rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() + for r in rel_opts.fields: + if r.name == rel_name: + e.add(opts, "'%s' m2m accessor name '%s.%s' clashes with another field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.many_to_many: + if r.name == rel_name: + e.add(opts, "'%s' m2m accessor name '%s.%s' clashes with a m2m field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.get_all_related_many_to_many_objects(): + if r.get_accessor_name() == rel_name and r.field is not f: + e.add(opts, "'%s' m2m accessor name '%s.%s' clashes with a related m2m field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + for r in rel_opts.get_all_related_objects(): + if r.get_accessor_name() == rel_name: + e.add(opts, "'%s' m2m accessor name '%s.%s' clashes with another related field. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) - # Check admin attribute. - if opts.admin is not None: - if not isinstance(opts.admin, meta.Admin): - e.add(opts, '"admin" attribute, if given, must be set to a meta.Admin() instance.') + # Check admin attribute. + if opts.admin is not None: + if not isinstance(opts.admin, models.AdminOptions): + e.add(opts, '"admin" attribute, if given, must be set to a models.AdminOptions() instance.') + else: + # list_display + if not isinstance(opts.admin.list_display, (list, tuple)): + e.add(opts, '"admin.list_display", if given, must be set to a list or tuple.') else: - # list_display - if not isinstance(opts.admin.list_display, (list, tuple)): - e.add(opts, '"admin.list_display", if given, must be set to a list or tuple.') - else: - for fn in opts.admin.list_display: - try: - f = opts.get_field(fn) - except meta.FieldDoesNotExist: - klass = opts.get_model_module().Klass - if not hasattr(klass, fn) or not callable(getattr(klass, fn)): - e.add(opts, '"admin.list_display" refers to %r, which isn\'t a field or method.' % fn) - else: - if isinstance(f, meta.ManyToManyField): - e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn) - # list_filter - if not isinstance(opts.admin.list_filter, (list, tuple)): - e.add(opts, '"admin.list_filter", if given, must be set to a list or tuple.') - else: - for fn in opts.admin.list_filter: - try: - f = opts.get_field(fn) - except meta.FieldDoesNotExist: - e.add(opts, '"admin.list_filter" refers to %r, which isn\'t a field.' % fn) + for fn in opts.admin.list_display: + try: + f = opts.get_field(fn) + except models.FieldDoesNotExist: + if not hasattr(cls, fn): + e.add(opts, '"admin.list_display" refers to %r, which isn\'t an attribute, method or property.' % fn) + else: + if isinstance(f, models.ManyToManyField): + e.add(opts, '"admin.list_display" doesn\'t support ManyToManyFields (%r).' % fn) + # list_filter + if not isinstance(opts.admin.list_filter, (list, tuple)): + e.add(opts, '"admin.list_filter", if given, must be set to a list or tuple.') + else: + for fn in opts.admin.list_filter: + try: + f = opts.get_field(fn) + except models.FieldDoesNotExist: + e.add(opts, '"admin.list_filter" refers to %r, which isn\'t a field.' % fn) - # Check ordering attribute. - if opts.ordering: - for field_name in opts.ordering: - if field_name == '?': continue - if field_name.startswith('-'): - field_name = field_name[1:] - if opts.order_with_respect_to and field_name == '_order': - continue - try: - opts.get_field(field_name, many_to_many=False) - except meta.FieldDoesNotExist: - e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name) + # Check ordering attribute. + if opts.ordering: + for field_name in opts.ordering: + if field_name == '?': continue + if field_name.startswith('-'): + field_name = field_name[1:] + if opts.order_with_respect_to and field_name == '_order': + continue + try: + opts.get_field(field_name, many_to_many=False) + except models.FieldDoesNotExist: + e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name) + + # Check core=True, if needed. + for related in opts.get_followed_related_objects(): + try: + for f in related.opts.fields: + if f.core: + raise StopIteration + e.add(related.opts, "At least one field in %s should have core=True, because it's being edited inline by %s.%s." % (related.opts.object_name, opts.module_name, opts.object_name)) + except StopIteration: + pass - # Check core=True, if needed. - for related in opts.get_followed_related_objects(): + # Check unique_together. + for ut in opts.unique_together: + for field_name in ut: try: - for f in related.opts.fields: - if f.core: - raise StopIteration - e.add(related.opts, "At least one field in %s should have core=True, because it's being edited inline by %s.%s." % (related.opts.object_name, opts.module_name, opts.object_name)) - except StopIteration: - pass + f = opts.get_field(field_name, many_to_many=True) + except models.FieldDoesNotExist: + e.add(opts, '"unique_together" refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name) + else: + if isinstance(f.rel, models.ManyToManyRel): + e.add(opts, '"unique_together" refers to %s. ManyToManyFields are not supported in unique_together.' % f.name) - # Check unique_together. - for ut in opts.unique_together: - for field_name in ut: - try: - f = opts.get_field(field_name, many_to_many=True) - except meta.FieldDoesNotExist: - e.add(opts, '"unique_together" refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name) - else: - if isinstance(f.rel, meta.ManyToManyRel): - e.add(opts, '"unique_together" refers to %s. ManyToManyFields are not supported in unique_together.' % f.name) return len(e.errors) def validate(outfile=sys.stdout): "Validates all installed models." - num_errors = get_validation_errors(outfile) - outfile.write('%s error%s found.\n' % (num_errors, num_errors != 1 and 's' or '')) + try: + num_errors = get_validation_errors(outfile) + outfile.write('%s error%s found.\n' % (num_errors, num_errors != 1 and 's' or '')) + except ImproperlyConfigured: + outfile.write("Skipping validation because things aren't configured properly.") validate.args = '' +def _check_for_validation_errors(app=None): + """Check that an app has no validation errors, and exit with errors if it does.""" + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + s = StringIO() + num_errors = get_validation_errors(s, app) + if num_errors: + sys.stderr.write(style.ERROR("Error: %s couldn't be installed, because there were errors in your model:\n" % app)) + s.seek(0) + sys.stderr.write(s.read()) + sys.exit(1) + def runserver(addr, port): "Starts a lightweight Web server for development." from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException @@ -798,13 +967,13 @@ def runserver(addr, port): if not addr: addr = '127.0.0.1' if not port.isdigit(): - sys.stderr.write("Error: %r is not a valid port number.\n" % port) + sys.stderr.write(style.ERROR("Error: %r is not a valid port number.\n" % port)) sys.exit(1) def inner_run(): - from django.conf.settings import SETTINGS_MODULE + from django.conf import settings print "Validating models..." validate() - print "\nDjango version %s, using settings %r" % (get_version(), SETTINGS_MODULE) + print "\nDjango version %s, using settings %r" % (get_version(), settings.SETTINGS_MODULE) print "Development server is running at http://%s:%s/" % (addr, port) print "Quit the server with CONTROL-C (Unix) or CTRL-BREAK (Windows)." try: @@ -820,7 +989,7 @@ def runserver(addr, port): error_text = ERRORS[e.args[0].args[0]] except (AttributeError, KeyError): error_text = str(e) - sys.stderr.write("Error: %s\n" % error_text) + sys.stderr.write(style.ERROR("Error: %s" % error_text) + '\n') sys.exit(1) except KeyboardInterrupt: sys.exit(0) @@ -830,17 +999,18 @@ runserver.args = '[optional port number, or ipaddr:port]' def createcachetable(tablename): "Creates the table needed to use the SQL cache backend" - from django.core import db, meta + from django.db import backend, connection, transaction, get_creation_module, models + data_types = get_creation_module().DATA_TYPES fields = ( # "key" is a reserved word in MySQL, so use "cache_key" instead. - meta.CharField(name='cache_key', maxlength=255, unique=True, primary_key=True), - meta.TextField(name='value'), - meta.DateTimeField(name='expires', db_index=True), + models.CharField(name='cache_key', maxlength=255, unique=True, primary_key=True), + models.TextField(name='value'), + models.DateTimeField(name='expires', db_index=True), ) table_output = [] index_output = [] for f in fields: - field_output = [db.db.quote_name(f.column), db.DATA_TYPES[f.get_internal_type()] % f.__dict__] + field_output = [backend.quote_name(f.name), data_types[f.get_internal_type()] % f.__dict__] field_output.append("%sNULL" % (not f.null and "NOT " or "")) if f.unique: field_output.append("UNIQUE") @@ -849,18 +1019,18 @@ def createcachetable(tablename): if f.db_index: unique = f.unique and "UNIQUE " or "" index_output.append("CREATE %sINDEX %s_%s ON %s (%s);" % \ - (unique, tablename, f.column, db.db.quote_name(tablename), - db.db.quote_name(f.column))) + (unique, tablename, f.name, backend.quote_name(tablename), + backend.quote_name(f.name))) table_output.append(" ".join(field_output)) - full_statement = ["CREATE TABLE %s (" % db.db.quote_name(tablename)] + full_statement = ["CREATE TABLE %s (" % backend.quote_name(tablename)] for i, line in enumerate(table_output): full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) full_statement.append(');') - curs = db.db.cursor() + curs = connection.cursor() curs.execute("\n".join(full_statement)) for statement in index_output: curs.execute(statement) - db.db.commit() + transaction.commit_unless_managed() createcachetable.args = "[tablename]" def run_shell(use_plain=False): @@ -877,17 +1047,22 @@ def run_shell(use_plain=False): code.interact() run_shell.args = '[--plain]' +def dbshell(): + "Runs the command-line client for the current DATABASE_ENGINE." + from django.db import runshell + runshell() +dbshell.args = "" + # Utilities for command-line script DEFAULT_ACTION_MAPPING = { 'adminindex': get_admin_index, - 'createsuperuser': createsuperuser, 'createcachetable' : createcachetable, -# 'dbcheck': database_check, - 'init': init, + 'dbshell': dbshell, + 'diffsettings': diffsettings, 'inspectdb': inspectdb, 'install': install, - 'installperms': installperms, + 'reset': reset, 'runserver': runserver, 'shell': run_shell, 'sql': get_sql_create, @@ -899,10 +1074,20 @@ DEFAULT_ACTION_MAPPING = { 'sqlsequencereset': get_sql_sequence_reset, 'startapp': startapp, 'startproject': startproject, + 'syncdb': syncdb, 'validate': validate, } -NO_SQL_TRANSACTION = ('adminindex', 'createcachetable', 'dbcheck', 'install', 'installperms', 'sqlindexes') +NO_SQL_TRANSACTION = ( + 'adminindex', + 'createcachetable', + 'dbshell', + 'diffsettings', + 'install', + 'reset', + 'sqlindexes', + 'syncdb', +) class DjangoOptionParser(OptionParser): def print_usage_and_exit(self): @@ -914,18 +1099,18 @@ def get_usage(action_mapping): Returns a usage string. Doesn't do the options stuff, because optparse takes care of that. """ - usage = ["usage: %prog action [options]\nactions:"] + usage = ["%prog action [options]\nactions:"] available_actions = action_mapping.keys() available_actions.sort() for a in available_actions: func = action_mapping[a] usage.append(" %s %s" % (a, func.args)) - usage.extend(textwrap.wrap(getattr(func, 'help_doc', func.__doc__), initial_indent=' ', subsequent_indent=' ')) + usage.extend(textwrap.wrap(getattr(func, 'help_doc', textwrap.dedent(func.__doc__.strip())), initial_indent=' ', subsequent_indent=' ')) usage.append("") return '\n'.join(usage[:-1]) # Cut off last list element, an empty space. def print_error(msg, cmd): - sys.stderr.write('Error: %s\nRun "%s --help" for help.\n' % (msg, cmd)) + sys.stderr.write(style.ERROR('Error: %s' % msg) + '\nRun "%s --help" for help.\n' % cmd) sys.exit(1) def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING): @@ -961,31 +1146,16 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING): from django.utils import translation translation.activate('en-us') - if action == 'createsuperuser': - try: - username, email, password = args[1], args[2], args[3] - except IndexError: - if len(args) == 1: # We got no arguments, just the action. - action_mapping[action]() - else: - sys.stderr.write("Error: %r requires arguments of 'username email password' or no argument at all.\n") - sys.exit(1) - else: - action_mapping[action](username, email, password) - elif action == 'shell': + if action == 'shell': action_mapping[action](options.plain is True) - elif action in ('init', 'validate'): + elif action in ('syncdb', 'validate', 'diffsettings', 'dbshell'): action_mapping[action]() elif action == 'inspectdb': try: - param = args[1] - except IndexError: - parser.print_usage_and_exit() - try: - for line in action_mapping[action](param): + for line in action_mapping[action](): print line except NotImplementedError: - sys.stderr.write("Error: %r isn't supported for the currently selected database backend.\n" % action) + sys.stderr.write(style.ERROR("Error: %r isn't supported for the currently selected database backend.\n" % action)) sys.exit(1) elif action == 'createcachetable': try: @@ -1009,25 +1179,22 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING): addr, port = '', args[1] action_mapping[action](addr, port) else: - from django.core import meta - if action == 'dbcheck': - mod_list = meta.get_all_installed_modules() - else: - try: - mod_list = [meta.get_app(app_label) for app_label in args[1:]] - except ImportError, e: - sys.stderr.write("Error: %s. Are you sure your INSTALLED_APPS setting is correct?\n" % e) - sys.exit(1) - if not mod_list: - parser.print_usage_and_exit() + from django.db import models + try: + mod_list = [models.get_app(app_label) for app_label in args[1:]] + except ImportError, e: + sys.stderr.write(style.ERROR("Error: %s. Are you sure your INSTALLED_APPS setting is correct?\n" % e)) + sys.exit(1) + if not mod_list: + parser.print_usage_and_exit() if action not in NO_SQL_TRANSACTION: - print "BEGIN;" + print style.SQL_KEYWORD("BEGIN;") for mod in mod_list: output = action_mapping[action](mod) if output: print '\n'.join(output) if action not in NO_SQL_TRANSACTION: - print "COMMIT;" + print style.SQL_KEYWORD("COMMIT;") def execute_manager(settings_mod): # Add this project to sys.path so that it's importable in the conventional @@ -1042,10 +1209,18 @@ def execute_manager(settings_mod): # Set DJANGO_SETTINGS_MODULE appropriately. os.environ['DJANGO_SETTINGS_MODULE'] = '%s.settings' % project_name + action_mapping = DEFAULT_ACTION_MAPPING.copy() + # Remove the "startproject" command from the action_mapping, because that's # a django-admin.py command, not a manage.py command. - action_mapping = DEFAULT_ACTION_MAPPING.copy() del action_mapping['startproject'] + # Override the startapp handler so that it always uses the + # project_directory, not the current working directory (which is default). + action_mapping['startapp'] = lambda app_name, directory: startapp(app_name, project_directory) + action_mapping['startapp'].__doc__ = startapp.__doc__ + action_mapping['startapp'].help_doc = startapp.help_doc + action_mapping['startapp'].args = startapp.args + # Run the django-admin.py command. execute_from_command_line(action_mapping) diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py deleted file mode 100644 index 8121736096..0000000000 --- a/django/core/meta/__init__.py +++ /dev/null @@ -1,1983 +0,0 @@ -from django.conf import settings -from django.core import formfields, validators -from django.core import db -from django.core.exceptions import ObjectDoesNotExist -from django.core.meta.fields import * -from django.utils.functional import curry -from django.utils.text import capfirst -import copy, datetime, os, re, sys, types - -# Admin stages. -ADD, CHANGE, BOTH = 1, 2, 3 - -# Size of each "chunk" for get_iterator calls. -# Larger values are slightly faster at the expense of more storage space. -GET_ITERATOR_CHUNK_SIZE = 100 - -# Prefix (in Python path style) to location of models. -MODEL_PREFIX = 'django.models' - -# Methods on models with the following prefix will be removed and -# converted to module-level functions. -MODEL_FUNCTIONS_PREFIX = '_module_' - -# Methods on models with the following prefix will be removed and -# converted to manipulator methods. -MANIPULATOR_FUNCTIONS_PREFIX = '_manipulator_' - -LOOKUP_SEPARATOR = '__' - -#################### -# HELPER FUNCTIONS # -#################### - -# Django currently supports two forms of ordering. -# Form 1 (deprecated) example: -# order_by=(('pub_date', 'DESC'), ('headline', 'ASC'), (None, 'RANDOM')) -# Form 2 (new-style) example: -# order_by=('-pub_date', 'headline', '?') -# Form 1 is deprecated and will no longer be supported for Django's first -# official release. The following code converts from Form 1 to Form 2. - -LEGACY_ORDERING_MAPPING = {'ASC': '_', 'DESC': '-_', 'RANDOM': '?'} - -def handle_legacy_orderlist(order_list): - if not order_list or isinstance(order_list[0], basestring): - return order_list - else: - import warnings - new_order_list = [LEGACY_ORDERING_MAPPING[j.upper()].replace('_', str(i)) for i, j in order_list] - warnings.warn("%r ordering syntax is deprecated. Use %r instead." % (order_list, new_order_list), DeprecationWarning) - return new_order_list - -def orderfield2column(f, opts): - try: - return opts.get_field(f, False).column - except FieldDoesNotExist: - return f - -def orderlist2sql(order_list, opts, prefix=''): - if prefix.endswith('.'): - prefix = db.db.quote_name(prefix[:-1]) + '.' - output = [] - for f in handle_legacy_orderlist(order_list): - if f.startswith('-'): - output.append('%s%s DESC' % (prefix, db.db.quote_name(orderfield2column(f[1:], opts)))) - elif f == '?': - output.append(db.get_random_function_sql()) - else: - output.append('%s%s ASC' % (prefix, db.db.quote_name(orderfield2column(f, opts)))) - return ', '.join(output) - -def get_module(app_label, module_name): - return __import__('%s.%s.%s' % (MODEL_PREFIX, app_label, module_name), '', '', ['']) - -def get_app(app_label): - return __import__('%s.%s' % (MODEL_PREFIX, app_label), '', '', ['']) - -_installed_models_cache = None -def get_installed_models(): - """ - Returns a list of installed "models" packages, such as foo.models, - ellington.news.models, etc. This does NOT include django.models. - """ - global _installed_models_cache - if _installed_models_cache is not None: - return _installed_models_cache - _installed_models_cache = [] - for a in settings.INSTALLED_APPS: - try: - _installed_models_cache.append(__import__(a + '.models', '', '', [''])) - except ImportError: - pass - return _installed_models_cache - -_installed_modules_cache = None -def get_installed_model_modules(core_models=None): - """ - Returns a list of installed models, such as django.models.core, - ellington.news.models.news, foo.models.bar, etc. - """ - global _installed_modules_cache - if _installed_modules_cache is not None: - return _installed_modules_cache - _installed_modules_cache = [] - - # django.models is a special case. - for submodule in (core_models or []): - _installed_modules_cache.append(__import__('django.models.%s' % submodule, '', '', [''])) - for m in get_installed_models(): - for submodule in getattr(m, '__all__', []): - mod = __import__('django.models.%s' % submodule, '', '', ['']) - try: - mod._MODELS - except AttributeError: - pass # Skip model modules that don't actually have models in them. - else: - _installed_modules_cache.append(mod) - return _installed_modules_cache - -class LazyDate: - """ - Use in limit_choices_to to compare the field to dates calculated at run time - instead of when the model is loaded. For example:: - - ... limit_choices_to = {'date__gt' : meta.LazyDate(days=-3)} ... - - which will limit the choices to dates greater than three days ago. - """ - def __init__(self, **kwargs): - self.delta = datetime.timedelta(**kwargs) - - def __str__(self): - return str(self.__get_value__()) - - def __repr__(self): - return "<LazyDate: %s>" % self.delta - - def __get_value__(self): - return datetime.datetime.now() + self.delta - -################ -# MAIN CLASSES # -################ - -class FieldDoesNotExist(Exception): - pass - -class BadKeywordArguments(Exception): - pass - -class BoundRelatedObject(object): - def __init__(self, related_object, field_mapping, original): - self.relation = related_object - self.field_mappings = field_mapping[related_object.opts.module_name] - - def template_name(self): - raise NotImplementedError - - def __repr__(self): - return repr(self.__dict__) - -class RelatedObject(object): - def __init__(self, parent_opts, opts, field): - self.parent_opts = parent_opts - self.opts = opts - self.field = field - self.edit_inline = field.rel.edit_inline - self.name = opts.module_name - self.var_name = opts.object_name.lower() - - def flatten_data(self, follow, obj=None): - new_data = {} - rel_instances = self.get_list(obj) - for i, rel_instance in enumerate(rel_instances): - instance_data = {} - for f in self.opts.fields + self.opts.many_to_many: - # TODO: Fix for recursive manipulators. - fol = follow.get(f.name, None) - if fol: - field_data = f.flatten_data(fol, rel_instance) - for name, value in field_data.items(): - instance_data['%s.%d.%s' % (self.var_name, i, name)] = value - new_data.update(instance_data) - return new_data - - def extract_data(self, data): - """ - Pull out the data meant for inline objects of this class, - i.e. anything starting with our module name. - """ - return data # TODO - - def get_list(self, parent_instance=None): - "Get the list of this type of object from an instance of the parent class." - if parent_instance != None: - func_name = 'get_%s_list' % self.get_method_name_part() - func = getattr(parent_instance, func_name) - list = func() - - count = len(list) + self.field.rel.num_extra_on_change - if self.field.rel.min_num_in_admin: - count = max(count, self.field.rel.min_num_in_admin) - if self.field.rel.max_num_in_admin: - count = min(count, self.field.rel.max_num_in_admin) - - change = count - len(list) - if change > 0: - return list + [None] * change - if change < 0: - return list[:change] - else: # Just right - return list - else: - return [None] * self.field.rel.num_in_admin - - def editable_fields(self): - "Get the fields in this class that should be edited inline." - return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] - - def get_follow(self, override=None): - if isinstance(override, bool): - if override: - over = {} - else: - return None - else: - if override: - over = override.copy() - elif self.edit_inline: - over = {} - else: - return None - - over[self.field.name] = False - return self.opts.get_follow(over) - - def __repr__(self): - return "<RelatedObject: %s related to %s>" % ( self.name, self.field.name) - - def get_manipulator_fields(self, opts, manipulator, change, follow): - # TODO: Remove core fields stuff. - if change: - meth_name = 'get_%s_count' % self.get_method_name_part() - count = getattr(manipulator.original_object, meth_name)() - count += self.field.rel.num_extra_on_change - if self.field.rel.min_num_in_admin: - count = max(count, self.field.rel.min_num_in_admin) - if self.field.rel.max_num_in_admin: - count = min(count, self.field.rel.max_num_in_admin) - else: - count = self.field.rel.num_in_admin - - fields = [] - for i in range(count): - for f in self.opts.fields + self.opts.many_to_many: - if follow.get(f.name, False): - prefix = '%s.%d.' % (self.var_name, i) - fields.extend(f.get_manipulator_fields(self.opts, manipulator, change, name_prefix=prefix, rel=True)) - return fields - - def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): - return bound_related_object_class(self, field_mapping, original) - - def get_method_name_part(self): - # This method encapsulates the logic that decides what name to give a - # method that retrieves related many-to-one or many-to-many objects. - # Usually it just uses the lower-cased object_name, but if the related - # object is in another app, the related object's app_label is appended. - # - # Examples: - # - # # Normal case -- a related object in the same app. - # # This method returns "choice". - # Poll.get_choice_list() - # - # # A related object in a different app. - # # This method returns "lcom_bestofaward". - # Place.get_lcom_bestofaward_list() # "lcom_bestofaward" - rel_obj_name = self.field.rel.related_name or self.opts.object_name.lower() - if self.parent_opts.app_label != self.opts.app_label: - rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name) - return rel_obj_name - -class QBase: - "Base class for QAnd and QOr" - def __init__(self, *args): - self.args = args - - def __repr__(self): - return '(%s)' % self.operator.join([repr(el) for el in self.args]) - - def get_sql(self, opts, table_count): - tables, join_where, where, params = [], [], [], [] - for val in self.args: - tables2, join_where2, where2, params2, table_count = val.get_sql(opts, table_count) - tables.extend(tables2) - join_where.extend(join_where2) - where.extend(where2) - params.extend(params2) - return tables, join_where, ['(%s)' % self.operator.join(where)], params, table_count - -class QAnd(QBase): - "Encapsulates a combined query that uses 'AND'." - operator = ' AND ' - def __or__(self, other): - if isinstance(other, (QAnd, QOr, Q)): - return QOr(self, other) - else: - raise TypeError, other - - def __and__(self, other): - if isinstance(other, QAnd): - return QAnd(*(self.args+other.args)) - elif isinstance(other, (Q, QOr)): - return QAnd(*(self.args+(other,))) - else: - raise TypeError, other - -class QOr(QBase): - "Encapsulates a combined query that uses 'OR'." - operator = ' OR ' - def __and__(self, other): - if isinstance(other, (QAnd, QOr, Q)): - return QAnd(self, other) - else: - raise TypeError, other - - def __or__(self, other): - if isinstance(other, QOr): - return QOr(*(self.args+other.args)) - elif isinstance(other, (Q, QAnd)): - return QOr(*(self.args+(other,))) - else: - raise TypeError, other - -class Q: - "Encapsulates queries for the 'complex' parameter to Django API functions." - def __init__(self, **kwargs): - self.kwargs = kwargs - - def __repr__(self): - return 'Q%r' % self.kwargs - - def __and__(self, other): - if isinstance(other, (Q, QAnd, QOr)): - return QAnd(self, other) - else: - raise TypeError, other - - def __or__(self, other): - if isinstance(other, (Q, QAnd, QOr)): - return QOr(self, other) - else: - raise TypeError, other - - def get_sql(self, opts, table_count): - return _parse_lookup(self.kwargs.items(), opts, table_count) - -class Options: - def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='', - fields=None, ordering=None, unique_together=None, admin=None, has_related_links=False, - where_constraints=None, object_name=None, app_label=None, - exceptions=None, permissions=None, get_latest_by=None, - order_with_respect_to=None, module_constants=None): - - # Save the original function args, for use by copy(). Note that we're - # NOT using copy.deepcopy(), because that would create a new copy of - # everything in memory, and it's better to conserve memory. Of course, - # this comes with the important gotcha that changing any attribute of - # this object will change its value in self._orig_init_args, so we - # need to be careful not to do that. In practice, we can pull this off - # because Options are generally read-only objects, and __init__() is - # the only place where its attributes are manipulated. - - # locals() is used purely for convenience, so we don't have to do - # something verbose like this: - # self._orig_init_args = { - # 'module_name': module_name, - # 'verbose_name': verbose_name, - # ... - # } - self._orig_init_args = locals() - del self._orig_init_args['self'] # because we don't care about it. - - # Move many-to-many related fields from self.fields into self.many_to_many. - self.fields, self.many_to_many = [], [] - for field in (fields or []): - if field.rel and isinstance(field.rel, ManyToManyRel): - self.many_to_many.append(field) - else: - self.fields.append(field) - self.module_name, self.verbose_name = module_name, verbose_name - self.verbose_name_plural = verbose_name_plural or verbose_name + 's' - self.db_table, self.has_related_links = db_table, has_related_links - self.ordering = ordering or [] - self.unique_together = unique_together or [] - self.where_constraints = where_constraints or [] - self.exceptions = exceptions or [] - self.permissions = permissions or [] - self.object_name, self.app_label = object_name, app_label - self.get_latest_by = get_latest_by - if order_with_respect_to: - self.order_with_respect_to = self.get_field(order_with_respect_to) - self.ordering = ('_order',) - else: - self.order_with_respect_to = None - self.module_constants = module_constants or {} - self.admin = admin - - # Calculate one_to_one_field. - self.one_to_one_field = None - for f in self.fields: - if isinstance(f.rel, OneToOneRel): - self.one_to_one_field = f - break - # Cache the primary-key field. - self.pk = None - for f in self.fields: - if f.primary_key: - self.pk = f - break - # If a primary_key field hasn't been specified, add an - # auto-incrementing primary-key ID field automatically. - if self.pk is None: - self.fields.insert(0, AutoField(name='id', verbose_name='ID', primary_key=True)) - self.pk = self.fields[0] - # Cache whether this has an AutoField. - self.has_auto_field = False - for f in self.fields: - is_auto = isinstance(f, AutoField) - if is_auto and self.has_auto_field: - raise AssertionError, "A model can't have more than one AutoField." - elif is_auto: - self.has_auto_field = True - - def __repr__(self): - return '<Options for %s>' % self.module_name - - def copy(self, **kwargs): - args = self._orig_init_args.copy() - args.update(kwargs) - return self.__class__(**args) - - def get_model_module(self): - return get_module(self.app_label, self.module_name) - - def get_content_type_id(self): - "Returns the content-type ID for this object type." - if not hasattr(self, '_content_type_id'): - mod = get_module('core', 'contenttypes') - self._content_type_id = mod.get_object(python_module_name__exact=self.module_name, package__label__exact=self.app_label).id - return self._content_type_id - - def get_field(self, name, many_to_many=True): - """ - Returns the requested field by name. Raises FieldDoesNotExist on error. - """ - to_search = many_to_many and (self.fields + self.many_to_many) or self.fields - for f in to_search: - if f.name == name: - return f - raise FieldDoesNotExist, "name=%s" % name - - def get_order_sql(self, table_prefix=''): - "Returns the full 'ORDER BY' clause for this object, according to self.ordering." - if not self.ordering: return '' - pre = table_prefix and (table_prefix + '.') or '' - return 'ORDER BY ' + orderlist2sql(self.ordering, self, pre) - - def get_add_permission(self): - return 'add_%s' % self.object_name.lower() - - def get_change_permission(self): - return 'change_%s' % self.object_name.lower() - - def get_delete_permission(self): - return 'delete_%s' % self.object_name.lower() - - def get_all_related_objects(self): - try: # Try the cache first. - return self._all_related_objects - except AttributeError: - module_list = get_installed_model_modules() - rel_objs = [] - for mod in module_list: - for klass in mod._MODELS: - for f in klass._meta.fields: - if f.rel and self == f.rel.to: - rel_objs.append(RelatedObject(self, klass._meta, f)) - if self.has_related_links: - # Manually add RelatedLink objects, which are a special case. - relatedlinks = get_module('relatedlinks', 'relatedlinks') - # Note that the copy() is very important -- otherwise any - # subsequently loaded object with related links will override this - # relationship we're adding. - link_field = copy.copy(relatedlinks.RelatedLink._meta.get_field('object_id')) - link_field.rel = ManyToOneRel(self.get_model_module().Klass, 'id', - num_in_admin=3, min_num_in_admin=3, edit_inline=TABULAR, - lookup_overrides={ - 'content_type__package__label__exact': self.app_label, - 'content_type__python_module_name__exact': self.module_name, - }) - rel_objs.append(RelatedObject(self, relatedlinks.RelatedLink._meta, link_field)) - self._all_related_objects = rel_objs - return rel_objs - - def get_followed_related_objects(self, follow=None): - if follow == None: - follow = self.get_follow() - return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] - - def get_data_holders(self, follow=None): - if follow == None: - follow = self.get_follow() - return [f for f in self.fields + self.many_to_many + self.get_all_related_objects() if follow.get(f.name, None)] - - def get_follow(self, override=None): - follow = {} - for f in self.fields + self.many_to_many + self.get_all_related_objects(): - if override and override.has_key(f.name): - child_override = override[f.name] - else: - child_override = None - fol = f.get_follow(child_override) - if fol: - follow[f.name] = fol - return follow - - def get_all_related_many_to_many_objects(self): - module_list = get_installed_model_modules() - rel_objs = [] - for mod in module_list: - for klass in mod._MODELS: - for f in klass._meta.many_to_many: - if f.rel and self == f.rel.to: - rel_objs.append(RelatedObject(self, klass._meta, f)) - return rel_objs - - def get_ordered_objects(self): - "Returns a list of Options objects that are ordered with respect to this object." - if not hasattr(self, '_ordered_objects'): - objects = [] - for klass in get_app(self.app_label)._MODELS: - opts = klass._meta - if opts.order_with_respect_to and opts.order_with_respect_to.rel \ - and self == opts.order_with_respect_to.rel.to: - objects.append(opts) - self._ordered_objects = objects - return self._ordered_objects - - def has_field_type(self, field_type, follow=None): - """ - Returns True if this object's admin form has at least one of the given - field_type (e.g. FileField). - """ - # TODO: follow - if not hasattr(self, '_field_types'): - self._field_types = {} - if not self._field_types.has_key(field_type): - try: - # First check self.fields. - for f in self.fields: - if isinstance(f, field_type): - raise StopIteration - # Failing that, check related fields. - for related in self.get_followed_related_objects(follow): - for f in related.opts.fields: - if isinstance(f, field_type): - raise StopIteration - except StopIteration: - self._field_types[field_type] = True - else: - self._field_types[field_type] = False - return self._field_types[field_type] - -def _reassign_globals(function_dict, extra_globals, namespace): - new_functions = {} - for k, v in function_dict.items(): - # Get the code object. - code = v.func_code - # Recreate the function, but give it access to extra_globals and the - # given namespace's globals, too. - new_globals = {'__builtins__': __builtins__, 'db': db.db, 'datetime': datetime} - new_globals.update(extra_globals.__dict__) - func = types.FunctionType(code, globals=new_globals, name=k, argdefs=v.func_defaults) - func.__dict__.update(v.__dict__) - setattr(namespace, k, func) - # For all of the custom functions that have been added so far, give - # them access to the new function we've just created. - for new_k, new_v in new_functions.items(): - new_v.func_globals[k] = func - new_functions[k] = func - -# Calculate the module_name using a poor-man's pluralization. -get_module_name = lambda class_name: class_name.lower() + 's' - -# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". -get_verbose_name = lambda class_name: re.sub('([A-Z])', ' \\1', class_name).lower().strip() - -class ModelBase(type): - "Metaclass for all models" - def __new__(cls, name, bases, attrs): - # If this isn't a subclass of Model, don't do anything special. - if not bases: - return type.__new__(cls, name, bases, attrs) - - try: - meta_attrs = attrs.pop('META').__dict__ - del meta_attrs['__module__'] - del meta_attrs['__doc__'] - except KeyError: - meta_attrs = {} - - # Gather all attributes that are Field instances. - fields = [] - for obj_name, obj in attrs.items(): - if isinstance(obj, Field): - obj.set_name(obj_name) - fields.append(obj) - del attrs[obj_name] - - # Sort the fields in the order that they were created. The - # "creation_counter" is needed because metaclasses don't preserve the - # attribute order. - fields.sort(lambda x, y: x.creation_counter - y.creation_counter) - - # If this model is a subclass of another model, create an Options - # object by first copying the base class's _meta and then updating it - # with the overrides from this class. - replaces_module = None - if bases[0] != Model: - field_names = [f.name for f in fields] - remove_fields = meta_attrs.pop('remove_fields', []) - for f in bases[0]._meta._orig_init_args['fields']: - if f.name not in field_names and f.name not in remove_fields: - fields.insert(0, f) - if meta_attrs.has_key('replaces_module'): - # Set the replaces_module variable for now. We can't actually - # do anything with it yet, because the module hasn't yet been - # created. - replaces_module = meta_attrs.pop('replaces_module').split('.') - # Pass any Options overrides to the base's Options instance, and - # simultaneously remove them from attrs. When this is done, attrs - # will be a dictionary of custom methods, plus __module__. - meta_overrides = {'fields': fields, 'module_name': get_module_name(name), 'verbose_name': get_verbose_name(name)} - for k, v in meta_attrs.items(): - if not callable(v) and k != '__module__': - meta_overrides[k] = meta_attrs.pop(k) - opts = bases[0]._meta.copy(**meta_overrides) - opts.object_name = name - del meta_overrides - else: - opts = Options( - module_name = meta_attrs.pop('module_name', get_module_name(name)), - # If the verbose_name wasn't given, use the class name, - # converted from InitialCaps to "lowercase with spaces". - verbose_name = meta_attrs.pop('verbose_name', get_verbose_name(name)), - verbose_name_plural = meta_attrs.pop('verbose_name_plural', ''), - db_table = meta_attrs.pop('db_table', ''), - fields = fields, - ordering = meta_attrs.pop('ordering', None), - unique_together = meta_attrs.pop('unique_together', None), - admin = meta_attrs.pop('admin', None), - has_related_links = meta_attrs.pop('has_related_links', False), - where_constraints = meta_attrs.pop('where_constraints', None), - object_name = name, - app_label = meta_attrs.pop('app_label', None), - exceptions = meta_attrs.pop('exceptions', None), - permissions = meta_attrs.pop('permissions', None), - get_latest_by = meta_attrs.pop('get_latest_by', None), - order_with_respect_to = meta_attrs.pop('order_with_respect_to', None), - module_constants = meta_attrs.pop('module_constants', None), - ) - - if meta_attrs != {}: - raise TypeError, "'class META' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()) - - # Dynamically create the module that will contain this class and its - # associated helper functions. - if replaces_module is not None: - new_mod = get_module(*replaces_module) - else: - new_mod = types.ModuleType(opts.module_name) - - # Collect any/all custom class methods and module functions, and move - # them to a temporary holding variable. We'll deal with them later. - if replaces_module is not None: - # Initialize these values to the base class' custom_methods and - # custom_functions. - custom_methods = dict([(k, v) for k, v in new_mod.Klass.__dict__.items() if hasattr(v, 'custom')]) - custom_functions = dict([(k, v) for k, v in new_mod.__dict__.items() if hasattr(v, 'custom')]) - else: - custom_methods, custom_functions = {}, {} - manipulator_methods = {} - for k, v in attrs.items(): - if k in ('__module__', '__init__', '_overrides', '__doc__'): - continue # Skip the important stuff. - assert callable(v), "%r is an invalid model parameter." % k - # Give the function a function attribute "custom" to designate that - # it's a custom function/method. - v.custom = True - if k.startswith(MODEL_FUNCTIONS_PREFIX): - custom_functions[k[len(MODEL_FUNCTIONS_PREFIX):]] = v - elif k.startswith(MANIPULATOR_FUNCTIONS_PREFIX): - manipulator_methods[k[len(MANIPULATOR_FUNCTIONS_PREFIX):]] = v - else: - custom_methods[k] = v - del attrs[k] - - # Create the module-level ObjectDoesNotExist exception. - dne_exc_name = '%sDoesNotExist' % name - does_not_exist_exception = types.ClassType(dne_exc_name, (ObjectDoesNotExist,), {}) - # Explicitly set its __module__ because it will initially (incorrectly) - # be set to the module the code is being executed in. - does_not_exist_exception.__module__ = MODEL_PREFIX + '.' + opts.module_name - setattr(new_mod, dne_exc_name, does_not_exist_exception) - - # Create other exceptions. - for exception_name in opts.exceptions: - exc = types.ClassType(exception_name, (Exception,), {}) - exc.__module__ = MODEL_PREFIX + '.' + opts.module_name # Set this explicitly, as above. - setattr(new_mod, exception_name, exc) - - # Create any module-level constants, if applicable. - for k, v in opts.module_constants.items(): - setattr(new_mod, k, v) - - # Create the default class methods. - attrs['__init__'] = curry(method_init, opts) - attrs['__eq__'] = curry(method_eq, opts) - attrs['__ne__'] = curry(method_ne, opts) - attrs['save'] = curry(method_save, opts) - attrs['save'].alters_data = True - attrs['delete'] = curry(method_delete, opts) - attrs['delete'].alters_data = True - - if opts.order_with_respect_to: - attrs['get_next_in_order'] = curry(method_get_next_in_order, opts, opts.order_with_respect_to) - attrs['get_previous_in_order'] = curry(method_get_previous_in_order, opts, opts.order_with_respect_to) - - for f in opts.fields: - # If the object has a relationship to itself, as designated by - # RECURSIVE_RELATIONSHIP_CONSTANT, create that relationship formally. - if f.rel and f.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT: - f.rel.to = opts - f.name = f.name or (f.rel.to.object_name.lower() + '_' + f.rel.to.pk.name) - f.verbose_name = f.verbose_name or f.rel.to.verbose_name - f.rel.field_name = f.rel.field_name or f.rel.to.pk.name - # Add "get_thingie" methods for many-to-one related objects. - # EXAMPLES: Choice.get_poll(), Story.get_dateline() - if isinstance(f.rel, ManyToOneRel): - func = curry(method_get_many_to_one, f) - func.__doc__ = "Returns the associated `%s.%s` object." % (f.rel.to.app_label, f.rel.to.module_name) - attrs['get_%s' % f.name] = func - - for f in opts.many_to_many: - # Add "get_thingie" methods for many-to-many related objects. - # EXAMPLES: Poll.get_site_list(), Story.get_byline_list() - func = curry(method_get_many_to_many, f) - func.__doc__ = "Returns a list of associated `%s.%s` objects." % (f.rel.to.app_label, f.rel.to.module_name) - attrs['get_%s_list' % f.rel.singular] = func - # Add "set_thingie" methods for many-to-many related objects. - # EXAMPLES: Poll.set_sites(), Story.set_bylines() - func = curry(method_set_many_to_many, f) - func.__doc__ = "Resets this object's `%s.%s` list to the given list of IDs. Note that it doesn't check whether the given IDs are valid." % (f.rel.to.app_label, f.rel.to.module_name) - func.alters_data = True - attrs['set_%s' % f.name] = func - - # Create the class, because we need it to use in currying. - new_class = type.__new__(cls, name, bases, attrs) - - # Give the class a docstring -- its definition. - if new_class.__doc__ is None: - new_class.__doc__ = "%s.%s(%s)" % (opts.module_name, name, ", ".join([f.name for f in opts.fields])) - - # Create the standard, module-level API helper functions such - # as get_object() and get_list(). - new_mod.get_object = curry(function_get_object, opts, new_class, does_not_exist_exception) - new_mod.get_object.__doc__ = "Returns the %s object matching the given parameters." % name - - new_mod.get_list = curry(function_get_list, opts, new_class) - new_mod.get_list.__doc__ = "Returns a list of %s objects matching the given parameters." % name - - new_mod.get_iterator = curry(function_get_iterator, opts, new_class) - new_mod.get_iterator.__doc__ = "Returns an iterator of %s objects matching the given parameters." % name - - new_mod.get_values = curry(function_get_values, opts, new_class) - new_mod.get_values.__doc__ = "Returns a list of dictionaries matching the given parameters." - - new_mod.get_values_iterator = curry(function_get_values_iterator, opts, new_class) - new_mod.get_values_iterator.__doc__ = "Returns an iterator of dictionaries matching the given parameters." - - new_mod.get_count = curry(function_get_count, opts) - new_mod.get_count.__doc__ = "Returns the number of %s objects matching the given parameters." % name - - new_mod._get_sql_clause = curry(function_get_sql_clause, opts) - - new_mod.get_in_bulk = curry(function_get_in_bulk, opts, new_class) - new_mod.get_in_bulk.__doc__ = "Returns a dictionary of ID -> %s for the %s objects with IDs in the given id_list." % (name, name) - - if opts.get_latest_by: - new_mod.get_latest = curry(function_get_latest, opts, new_class, does_not_exist_exception) - - for f in opts.fields: - #TODO : change this into a virtual function so that user defined fields will be able to add methods to module or class. - if f.choices: - # Add "get_thingie_display" method to get human-readable value. - func = curry(method_get_display_value, f) - setattr(new_class, 'get_%s_display' % f.name, func) - if isinstance(f, DateField) or isinstance(f, DateTimeField): - # Add "get_next_by_thingie" and "get_previous_by_thingie" methods - # for all DateFields and DateTimeFields that cannot be null. - # EXAMPLES: Poll.get_next_by_pub_date(), Poll.get_previous_by_pub_date() - if not f.null: - setattr(new_class, 'get_next_by_%s' % f.name, curry(method_get_next_or_previous, new_mod.get_object, opts, f, True)) - setattr(new_class, 'get_previous_by_%s' % f.name, curry(method_get_next_or_previous, new_mod.get_object, opts, f, False)) - # Add "get_thingie_list" for all DateFields and DateTimeFields. - # EXAMPLE: polls.get_pub_date_list() - func = curry(function_get_date_list, opts, f) - func.__doc__ = "Returns a list of days, months or years (as datetime.datetime objects) in which %s objects are available. The first parameter ('kind') must be one of 'year', 'month' or 'day'." % name - setattr(new_mod, 'get_%s_list' % f.name, func) - - elif isinstance(f, FileField): - setattr(new_class, 'get_%s_filename' % f.name, curry(method_get_file_filename, f)) - setattr(new_class, 'get_%s_url' % f.name, curry(method_get_file_url, f)) - setattr(new_class, 'get_%s_size' % f.name, curry(method_get_file_size, f)) - func = curry(method_save_file, f) - func.alters_data = True - setattr(new_class, 'save_%s_file' % f.name, func) - if isinstance(f, ImageField): - # Add get_BLAH_width and get_BLAH_height methods, but only - # if the image field doesn't have width and height cache - # fields. - if not f.width_field: - setattr(new_class, 'get_%s_width' % f.name, curry(method_get_image_width, f)) - if not f.height_field: - setattr(new_class, 'get_%s_height' % f.name, curry(method_get_image_height, f)) - - # Add the class itself to the new module we've created. - new_mod.__dict__[name] = new_class - - # Add "Klass" -- a shortcut reference to the class. - new_mod.__dict__['Klass'] = new_class - - # Add the Manipulators. - new_mod.__dict__['AddManipulator'] = get_manipulator(opts, new_class, manipulator_methods, add=True) - new_mod.__dict__['ChangeManipulator'] = get_manipulator(opts, new_class, manipulator_methods, change=True) - - # Now that we have references to new_mod and new_class, we can add - # any/all extra class methods to the new class. Note that we could - # have just left the extra methods in attrs (above), but that would - # have meant that any code within the extra methods would *not* have - # access to module-level globals, such as get_list(), db, etc. - # In order to give these methods access to those globals, we have to - # deconstruct the method getting its raw "code" object, then recreating - # the function with a new "globals" dictionary. - # - # To complicate matters more, because each method is manually assigned - # a "globals" value, that "globals" value does NOT include the methods - # that haven't been created yet. For instance, if there are two custom - # methods, foo() and bar(), and foo() is created first, it won't have - # bar() within its globals(). This is a problem because sometimes - # custom methods/functions refer to other custom methods/functions. To - # solve this problem, we keep track of the new functions created (in - # the new_functions variable) and manually append each new function to - # the func_globals() of all previously-created functions. So, by the - # end of the loop, all functions will "know" about all the other - # functions. - _reassign_globals(custom_methods, new_mod, new_class) - _reassign_globals(custom_functions, new_mod, new_mod) - _reassign_globals(manipulator_methods, new_mod, new_mod.__dict__['AddManipulator']) - _reassign_globals(manipulator_methods, new_mod, new_mod.__dict__['ChangeManipulator']) - - if hasattr(new_class, 'get_absolute_url'): - new_class.get_absolute_url = curry(get_absolute_url, opts, new_class.get_absolute_url) - - # Get a reference to the module the class is in, and dynamically add - # the new module to it. - app_package = sys.modules.get(new_class.__module__) - if replaces_module is not None: - app_label = replaces_module[0] - else: - app_package.__dict__[opts.module_name] = new_mod - app_label = app_package.__name__[app_package.__name__.rfind('.')+1:] - - # Populate the _MODELS member on the module the class is in. - # Example: django.models.polls will have a _MODELS member that will - # contain this list: - # [<class 'django.models.polls.Poll'>, <class 'django.models.polls.Choice'>] - # Don't do this if replaces_module is set. - app_package.__dict__.setdefault('_MODELS', []).append(new_class) - - # Cache the app label. - opts.app_label = app_label - - # If the db_table wasn't provided, use the app_label + module_name. - if not opts.db_table: - opts.db_table = "%s_%s" % (app_label, opts.module_name) - new_class._meta = opts - - # Set the __file__ attribute to the __file__ attribute of its package, - # because they're technically from the same file. Note: if we didn't - # set this, sys.modules would think this module was built-in. - try: - new_mod.__file__ = app_package.__file__ - except AttributeError: - # 'module' object has no attribute '__file__', which means the - # class was probably being entered via the interactive interpreter. - pass - - # Add the module's entry to sys.modules -- for instance, - # "django.models.polls.polls". Note that "django.models.polls" has already - # been added automatically. - sys.modules.setdefault('%s.%s.%s' % (MODEL_PREFIX, app_label, opts.module_name), new_mod) - - # If this module replaces another one, get a reference to the other - # module's parent, and replace the other module with the one we've just - # created. - if replaces_module is not None: - old_app = get_app(replaces_module[0]) - setattr(old_app, replaces_module[1], new_mod) - for i, model in enumerate(old_app._MODELS): - if model._meta.module_name == replaces_module[1]: - # Replace the appropriate member of the old app's _MODELS - # data structure. - old_app._MODELS[i] = new_class - # Replace all relationships to the old class with - # relationships to the new one. - for related in model._meta.get_all_related_objects() + model._meta.get_all_related_many_to_many_objects(): - related.field.rel.to = opts - break - return new_class - -class Model: - __metaclass__ = ModelBase - - def __repr__(self): - return '<%s object>' % self.__class__.__name__ - -############################################ -# HELPER FUNCTIONS (CURRIED MODEL METHODS) # -############################################ - -# CORE METHODS ############################# - -def method_init(opts, self, *args, **kwargs): - if kwargs: - for f in opts.fields: - if isinstance(f.rel, ManyToOneRel): - try: - # Assume object instance was passed in. - rel_obj = kwargs.pop(f.name) - except KeyError: - try: - # Object instance wasn't passed in -- must be an ID. - val = kwargs.pop(f.attname) - except KeyError: - val = f.get_default() - else: - # Object instance was passed in. - # Special case: You can pass in "None" for related objects if it's allowed. - if rel_obj is None and f.null: - val = None - else: - try: - val = getattr(rel_obj, f.rel.get_related_field().attname) - except AttributeError: - raise TypeError, "Invalid value: %r should be a %s instance, not a %s" % (f.name, f.rel.to, type(rel_obj)) - setattr(self, f.attname, val) - else: - val = kwargs.pop(f.attname, f.get_default()) - setattr(self, f.attname, val) - if kwargs: - raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] - for i, arg in enumerate(args): - setattr(self, opts.fields[i].attname, arg) - -def method_eq(opts, self, other): - return isinstance(other, self.__class__) and getattr(self, opts.pk.attname) == getattr(other, opts.pk.attname) - -def method_ne(opts, self, other): - return not method_eq(opts, self, other) - -def method_save(opts, self): - # Run any pre-save hooks. - if hasattr(self, '_pre_save'): - self._pre_save() - non_pks = [f for f in opts.fields if not f.primary_key] - cursor = db.db.cursor() - - # First, try an UPDATE. If that doesn't update anything, do an INSERT. - pk_val = getattr(self, opts.pk.attname) - pk_set = bool(pk_val) - record_exists = True - if pk_set: - # Determine whether a record with the primary key already exists. - cursor.execute("SELECT 1 FROM %s WHERE %s=%%s LIMIT 1" % \ - (db.db.quote_name(opts.db_table), db.db.quote_name(opts.pk.column)), [pk_val]) - # If it does already exist, do an UPDATE. - if cursor.fetchone(): - db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), False)) for f in non_pks] - cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \ - (db.db.quote_name(opts.db_table), - ','.join(['%s=%%s' % db.db.quote_name(f.column) for f in non_pks]), - db.db.quote_name(opts.pk.attname)), - db_values + [pk_val]) - else: - record_exists = False - if not pk_set or not record_exists: - field_names = [db.db.quote_name(f.column) for f in opts.fields if not isinstance(f, AutoField)] - db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), True)) for f in opts.fields if not isinstance(f, AutoField)] - # If the PK has been manually set we must respect that - if pk_set: - field_names += [f.column for f in opts.fields if isinstance(f, AutoField)] - db_values += [f.get_db_prep_save(f.pre_save(getattr(self, f.column), True)) for f in opts.fields if isinstance(f, AutoField)] - placeholders = ['%s'] * len(field_names) - if opts.order_with_respect_to: - field_names.append(db.db.quote_name('_order')) - # TODO: This assumes the database supports subqueries. - placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \ - (db.db.quote_name(opts.db_table), db.db.quote_name(opts.order_with_respect_to.column))) - db_values.append(getattr(self, opts.order_with_respect_to.attname)) - cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \ - (db.db.quote_name(opts.db_table), ','.join(field_names), - ','.join(placeholders)), db_values) - if opts.has_auto_field and not pk_set: - setattr(self, opts.pk.attname, db.get_last_insert_id(cursor, opts.db_table, opts.pk.column)) - db.db.commit() - # Run any post-save hooks. - if hasattr(self, '_post_save'): - self._post_save() - -def method_delete(opts, self): - assert getattr(self, opts.pk.attname) is not None, "%r can't be deleted because it doesn't have an ID." - # Run any pre-delete hooks. - if hasattr(self, '_pre_delete'): - self._pre_delete() - cursor = db.db.cursor() - for related in opts.get_all_related_objects(): - rel_opts_name = related.get_method_name_part() - if isinstance(related.field.rel, OneToOneRel): - try: - sub_obj = getattr(self, 'get_%s' % rel_opts_name)() - except ObjectDoesNotExist: - pass - else: - sub_obj.delete() - else: - for sub_obj in getattr(self, 'get_%s_list' % rel_opts_name)(): - sub_obj.delete() - for related in opts.get_all_related_many_to_many_objects(): - cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (db.db.quote_name(related.field.get_m2m_db_table(related.opts)), - db.db.quote_name(self._meta.object_name.lower() + '_id')), [getattr(self, opts.pk.attname)]) - for f in opts.many_to_many: - cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (db.db.quote_name(f.get_m2m_db_table(opts)), - db.db.quote_name(self._meta.object_name.lower() + '_id')), - [getattr(self, opts.pk.attname)]) - cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (db.db.quote_name(opts.db_table), db.db.quote_name(opts.pk.column)), - [getattr(self, opts.pk.attname)]) - db.db.commit() - setattr(self, opts.pk.attname, None) - for f in opts.fields: - if isinstance(f, FileField) and getattr(self, f.attname): - file_name = getattr(self, 'get_%s_filename' % f.name)() - # If the file exists and no other object of this type references it, - # delete it from the filesystem. - if os.path.exists(file_name) and not opts.get_model_module().get_list(**{'%s__exact' % f.name: getattr(self, f.name)}): - os.remove(file_name) - # Run any post-delete hooks. - if hasattr(self, '_post_delete'): - self._post_delete() - -def method_get_next_in_order(opts, order_field, self): - if not hasattr(self, '_next_in_order_cache'): - self._next_in_order_cache = opts.get_model_module().get_object(order_by=('_order',), - where=['%s > (SELECT %s FROM %s WHERE %s=%%s)' % \ - (db.db.quote_name('_order'), db.db.quote_name('_order'), - db.db.quote_name(opts.db_table), db.db.quote_name(opts.pk.column)), - '%s=%%s' % db.db.quote_name(order_field.column)], limit=1, - params=[getattr(self, opts.pk.attname), getattr(self, order_field.attname)]) - return self._next_in_order_cache - -def method_get_previous_in_order(opts, order_field, self): - if not hasattr(self, '_previous_in_order_cache'): - self._previous_in_order_cache = opts.get_model_module().get_object(order_by=('-_order',), - where=['%s < (SELECT %s FROM %s WHERE %s=%%s)' % \ - (db.db.quote_name('_order'), db.db.quote_name('_order'), - db.db.quote_name(opts.db_table), db.db.quote_name(opts.pk.column)), - '%s=%%s' % db.db.quote_name(order_field.column)], limit=1, - params=[getattr(self, opts.pk.attname), getattr(self, order_field.attname)]) - return self._previous_in_order_cache - -# RELATIONSHIP METHODS ##################### - -# Example: Story.get_dateline() -def method_get_many_to_one(field_with_rel, self): - cache_var = field_with_rel.get_cache_name() - if not hasattr(self, cache_var): - val = getattr(self, field_with_rel.attname) - mod = field_with_rel.rel.to.get_model_module() - if val is None: - raise getattr(mod, '%sDoesNotExist' % field_with_rel.rel.to.object_name) - other_field = field_with_rel.rel.get_related_field() - if other_field.rel: - params = {'%s__%s__exact' % (field_with_rel.rel.field_name, other_field.rel.field_name): val} - else: - params = {'%s__exact'% field_with_rel.rel.field_name: val} - retrieved_obj = mod.get_object(**params) - setattr(self, cache_var, retrieved_obj) - return getattr(self, cache_var) - -# Handles getting many-to-many related objects. -# Example: Poll.get_site_list() -def method_get_many_to_many(field_with_rel, self): - rel = field_with_rel.rel.to - cache_var = '_%s_cache' % field_with_rel.name - if not hasattr(self, cache_var): - mod = rel.get_model_module() - sql = "SELECT %s FROM %s a, %s b WHERE a.%s = b.%s AND b.%s = %%s %s" % \ - (','.join(['a.%s' % db.db.quote_name(f.column) for f in rel.fields]), - db.db.quote_name(rel.db_table), - db.db.quote_name(field_with_rel.get_m2m_db_table(self._meta)), - db.db.quote_name(rel.pk.column), - db.db.quote_name(rel.object_name.lower() + '_id'), - db.db.quote_name(self._meta.object_name.lower() + '_id'), rel.get_order_sql('a')) - cursor = db.db.cursor() - cursor.execute(sql, [getattr(self, self._meta.pk.attname)]) - setattr(self, cache_var, [getattr(mod, rel.object_name)(*row) for row in cursor.fetchall()]) - return getattr(self, cache_var) - -# Handles setting many-to-many relationships. -# Example: Poll.set_sites() -def method_set_many_to_many(rel_field, self, id_list): - current_ids = [getattr(obj, obj._meta.pk.attname) for obj in method_get_many_to_many(rel_field, self)] - ids_to_add, ids_to_delete = dict([(i, 1) for i in id_list]), [] - for current_id in current_ids: - if current_id in id_list: - del ids_to_add[current_id] - else: - ids_to_delete.append(current_id) - ids_to_add = ids_to_add.keys() - # Now ids_to_add is a list of IDs to add, and ids_to_delete is a list of IDs to delete. - if not ids_to_delete and not ids_to_add: - return False # No change - rel = rel_field.rel.to - m2m_table = rel_field.get_m2m_db_table(self._meta) - cursor = db.db.cursor() - this_id = getattr(self, self._meta.pk.attname) - if ids_to_delete: - sql = "DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (db.db.quote_name(m2m_table), - db.db.quote_name(self._meta.object_name.lower() + '_id'), - db.db.quote_name(rel.object_name.lower() + '_id'), ','.join(map(str, ids_to_delete))) - cursor.execute(sql, [this_id]) - if ids_to_add: - sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (db.db.quote_name(m2m_table), - db.db.quote_name(self._meta.object_name.lower() + '_id'), - db.db.quote_name(rel.object_name.lower() + '_id')) - cursor.executemany(sql, [(this_id, i) for i in ids_to_add]) - db.db.commit() - try: - delattr(self, '_%s_cache' % rel_field.name) # clear cache, if it exists - except AttributeError: - pass - return True - -# Handles related-object retrieval. -# Examples: Poll.get_choice(), Poll.get_choice_list(), Poll.get_choice_count() -def method_get_related(method_name, rel_mod, rel_field, self, **kwargs): - if self._meta.has_related_links and rel_mod.Klass._meta.module_name == 'relatedlinks': - kwargs['object_id__exact'] = getattr(self, rel_field.rel.field_name) - else: - kwargs['%s__%s__exact' % (rel_field.name, rel_field.rel.to.pk.name)] = getattr(self, rel_field.rel.get_related_field().attname) - kwargs.update(rel_field.rel.lookup_overrides) - related = getattr(rel_mod, method_name)(**kwargs) - - # Cache the 'self' object for backward links. - # Example: Each choice in Poll.get_choice_list() will have its poll cache filled. - # Pre-cache the self object, for following links back. - if method_name == 'get_list': - cache_name = rel_field.get_cache_name() - for obj in related: - setattr(obj, cache_name, self) - elif method_name == 'get_object': - setattr(related, rel_field.get_cache_name(), self) - return related - -# Handles adding related objects. -# Example: Poll.add_choice() -def method_add_related(rel_obj, rel_mod, rel_field, self, *args, **kwargs): - init_kwargs = dict(zip([f.attname for f in rel_obj.fields if f != rel_field and not isinstance(f, AutoField)], args)) - init_kwargs.update(kwargs) - for f in rel_obj.fields: - if isinstance(f, AutoField): - init_kwargs[f.attname] = None - init_kwargs[rel_field.name] = self - obj = rel_mod.Klass(**init_kwargs) - obj.save() - return obj - -# Handles related many-to-many object retrieval. -# Examples: Album.get_song(), Album.get_song_list(), Album.get_song_count() -def method_get_related_many_to_many(method_name, opts, rel_mod, rel_field, self, **kwargs): - kwargs['%s__%s__exact' % (rel_field.name, opts.pk.name)] = getattr(self, opts.pk.attname) - return getattr(rel_mod, method_name)(**kwargs) - -# Handles setting many-to-many related objects. -# Example: Album.set_songs() -def method_set_related_many_to_many(rel_opts, rel_field, self, id_list): - id_list = map(int, id_list) # normalize to integers - rel = rel_field.rel.to - m2m_table = rel_field.get_m2m_db_table(rel_opts) - this_id = getattr(self, self._meta.pk.attname) - cursor = db.db.cursor() - cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ - (db.db.quote_name(m2m_table), - db.db.quote_name(rel.object_name.lower() + '_id')), [this_id]) - sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (db.db.quote_name(m2m_table), - db.db.quote_name(rel.object_name.lower() + '_id'), - db.db.quote_name(rel_opts.object_name.lower() + '_id')) - cursor.executemany(sql, [(this_id, i) for i in id_list]) - db.db.commit() - -# ORDERING METHODS ######################### - -def method_set_order(ordered_obj, self, id_list): - cursor = db.db.cursor() - # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" - sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \ - (db.db.quote_name(ordered_obj.db_table), db.db.quote_name('_order'), - db.db.quote_name(ordered_obj.order_with_respect_to.column), - db.db.quote_name(ordered_obj.pk.column)) - rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) - cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) - db.db.commit() - -def method_get_order(ordered_obj, self): - cursor = db.db.cursor() - # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" - sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY %s" % \ - (db.db.quote_name(ordered_obj.pk.column), - db.db.quote_name(ordered_obj.db_table), - db.db.quote_name(ordered_obj.order_with_respect_to.column), - db.db.quote_name('_order')) - rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) - cursor.execute(sql, [rel_val]) - return [r[0] for r in cursor.fetchall()] - -# DATE-RELATED METHODS ##################### - -def method_get_next_or_previous(get_object_func, opts, field, is_next, self, **kwargs): - op = is_next and '>' or '<' - kwargs.setdefault('where', []).append('(%s %s %%s OR (%s = %%s AND %s.%s %s %%s))' % \ - (db.db.quote_name(field.column), op, db.db.quote_name(field.column), - db.db.quote_name(opts.db_table), db.db.quote_name(opts.pk.column), op)) - param = str(getattr(self, field.attname)) - kwargs.setdefault('params', []).extend([param, param, getattr(self, opts.pk.attname)]) - kwargs['order_by'] = [(not is_next and '-' or '') + field.name, (not is_next and '-' or '') + opts.pk.name] - kwargs['limit'] = 1 - return get_object_func(**kwargs) - -# CHOICE-RELATED METHODS ################### - -def method_get_display_value(field, self): - value = getattr(self, field.attname) - return dict(field.choices).get(value, value) - -# FILE-RELATED METHODS ##################### - -def method_get_file_filename(field, self): - return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)) - -def method_get_file_url(field, self): - if getattr(self, field.attname): # value is not blank - import urlparse - return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/') - return '' - -def method_get_file_size(field, self): - return os.path.getsize(method_get_file_filename(field, self)) - -def method_save_file(field, self, filename, raw_contents): - directory = field.get_directory_name() - try: # Create the date-based directory if it doesn't exist. - os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) - except OSError: # Directory probably already exists. - pass - filename = field.get_filename(filename) - - # If the filename already exists, keep adding an underscore to the name of - # the file until the filename doesn't exist. - while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)): - try: - dot_index = filename.rindex('.') - except ValueError: # filename has no dot - filename += '_' - else: - filename = filename[:dot_index] + '_' + filename[dot_index:] - - # Write the file to disk. - setattr(self, field.attname, filename) - fp = open(getattr(self, 'get_%s_filename' % field.name)(), 'wb') - fp.write(raw_contents) - fp.close() - - # Save the width and/or height, if applicable. - if isinstance(field, ImageField) and (field.width_field or field.height_field): - from django.utils.images import get_image_dimensions - width, height = get_image_dimensions(getattr(self, 'get_%s_filename' % field.name)()) - if field.width_field: - setattr(self, field.width_field, width) - if field.height_field: - setattr(self, field.height_field, height) - - # Save the object, because it has changed. - self.save() - -# IMAGE FIELD METHODS ###################### - -def method_get_image_width(field, self): - return _get_image_dimensions(field, self)[0] - -def method_get_image_height(field, self): - return _get_image_dimensions(field, self)[1] - -def _get_image_dimensions(field, self): - cachename = "__%s_dimensions_cache" % field.name - if not hasattr(self, cachename): - from django.utils.images import get_image_dimensions - fname = getattr(self, "get_%s_filename" % field.name)() - setattr(self, cachename, get_image_dimensions(fname)) - return getattr(self, cachename) - -############################################## -# HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) # -############################################## - -def get_absolute_url(opts, func, self): - return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self) - -def _get_where_clause(lookup_type, table_prefix, field_name, value): - if table_prefix.endswith('.'): - table_prefix = db.db.quote_name(table_prefix[:-1])+'.' - field_name = db.db.quote_name(field_name) - try: - return '%s%s %s' % (table_prefix, field_name, (db.OPERATOR_MAPPING[lookup_type] % '%s')) - except KeyError: - pass - if lookup_type == 'in': - return '%s%s IN (%s)' % (table_prefix, field_name, ','.join(['%s' for v in value])) - elif lookup_type == 'range': - return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name) - elif lookup_type in ('year', 'month', 'day'): - return "%s = %%s" % db.get_date_extract_sql(lookup_type, table_prefix + field_name) - elif lookup_type == 'isnull': - return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) - raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) - -def function_get_object(opts, klass, does_not_exist_exception, **kwargs): - kwargs['order_by'] = kwargs.get('order_by', ()) - obj_list = function_get_list(opts, klass, **kwargs) - if len(obj_list) < 1: - raise does_not_exist_exception, "%s does not exist for %s" % (opts.object_name, kwargs) - assert len(obj_list) == 1, "get_object() returned more than one %s -- it returned %s! Lookup parameters were %s" % (opts.object_name, len(obj_list), kwargs) - return obj_list[0] - -def _get_cached_row(opts, row, index_start): - "Helper function that recursively returns an object with cache filled" - index_end = index_start + len(opts.fields) - obj = opts.get_model_module().Klass(*row[index_start:index_end]) - for f in opts.fields: - if f.rel and not f.null: - rel_obj, index_end = _get_cached_row(f.rel.to, row, index_end) - setattr(obj, f.get_cache_name(), rel_obj) - return obj, index_end - -def function_get_iterator(opts, klass, **kwargs): - # kwargs['select'] is a dictionary, and dictionaries' key order is - # undefined, so we convert it to a list of tuples internally. - kwargs['select'] = kwargs.get('select', {}).items() - - cursor = db.db.cursor() - select, sql, params = function_get_sql_clause(opts, **kwargs) - cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params) - fill_cache = kwargs.get('select_related') - index_end = len(opts.fields) - while 1: - rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) - if not rows: - raise StopIteration - for row in rows: - if fill_cache: - obj, index_end = _get_cached_row(opts, row, 0) - else: - obj = klass(*row[:index_end]) - for i, k in enumerate(kwargs['select']): - setattr(obj, k[0], row[index_end+i]) - yield obj - -def function_get_list(opts, klass, **kwargs): - return list(function_get_iterator(opts, klass, **kwargs)) - -def function_get_count(opts, **kwargs): - kwargs['order_by'] = [] - kwargs['offset'] = None - kwargs['limit'] = None - kwargs['select_related'] = False - _, sql, params = function_get_sql_clause(opts, **kwargs) - cursor = db.db.cursor() - cursor.execute("SELECT COUNT(*)" + sql, params) - return cursor.fetchone()[0] - -def function_get_values_iterator(opts, klass, **kwargs): - # select_related and select aren't supported in get_values(). - kwargs['select_related'] = False - kwargs['select'] = {} - - # 'fields' is a list of field names to fetch. - try: - fields = [opts.get_field(f).column for f in kwargs.pop('fields')] - except KeyError: # Default to all fields. - fields = [f.column for f in opts.fields] - - cursor = db.db.cursor() - _, sql, params = function_get_sql_clause(opts, **kwargs) - select = ['%s.%s' % (db.db.quote_name(opts.db_table), db.db.quote_name(f)) for f in fields] - cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params) - while 1: - rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) - if not rows: - raise StopIteration - for row in rows: - yield dict(zip(fields, row)) - -def function_get_values(opts, klass, **kwargs): - return list(function_get_values_iterator(opts, klass, **kwargs)) - -def _fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen): - """ - Helper function that recursively populates the select, tables and where (in - place) for fill-cache queries. - """ - for f in opts.fields: - if f.rel and not f.null: - db_table = f.rel.to.db_table - if db_table not in cache_tables_seen: - tables.append(db.db.quote_name(db_table)) - else: # The table was already seen, so give it a table alias. - new_prefix = '%s%s' % (db_table, len(cache_tables_seen)) - tables.append('%s %s' % (db.db.quote_name(db_table), db.db.quote_name(new_prefix))) - db_table = new_prefix - cache_tables_seen.append(db_table) - where.append('%s.%s = %s.%s' % \ - (db.db.quote_name(old_prefix), db.db.quote_name(f.column), - db.db.quote_name(db_table), db.db.quote_name(f.rel.get_related_field().column))) - select.extend(['%s.%s' % (db.db.quote_name(db_table), db.db.quote_name(f2.column)) for f2 in f.rel.to.fields]) - _fill_table_cache(f.rel.to, select, tables, where, db_table, cache_tables_seen) - -def _throw_bad_kwarg_error(kwarg): - # Helper function to remove redundancy. - raise TypeError, "got unexpected keyword argument '%s'" % kwarg - -def _parse_lookup(kwarg_items, opts, table_count=0): - # Helper function that handles converting API kwargs (e.g. - # "name__exact": "tom") to SQL. - - # Note that there is a distinction between where and join_where. The latter - # is specifically a list of where clauses to use for JOINs. This - # distinction is necessary because of support for "_or". - - # table_count is used to ensure table aliases are unique. - tables, join_where, where, params = [], [], [], [] - for kwarg, kwarg_value in kwarg_items: - if kwarg in ('order_by', 'limit', 'offset', 'select_related', 'distinct', 'select', 'tables', 'where', 'params'): - continue - if kwarg_value is None: - continue - if kwarg == 'complex': - tables2, join_where2, where2, params2, table_count = kwarg_value.get_sql(opts, table_count) - tables.extend(tables2) - join_where.extend(join_where2) - where.extend(where2) - params.extend(params2) - continue - if kwarg == '_or': - for val in kwarg_value: - tables2, join_where2, where2, params2, table_count = _parse_lookup(val, opts, table_count) - tables.extend(tables2) - join_where.extend(join_where2) - where.append('(%s)' % ' OR '.join(where2)) - params.extend(params2) - continue - lookup_list = kwarg.split(LOOKUP_SEPARATOR) - # pk="value" is shorthand for (primary key)__exact="value" - if lookup_list[-1] == 'pk': - if opts.pk.rel: - lookup_list = lookup_list[:-1] + [opts.pk.name, opts.pk.rel.field_name, 'exact'] - else: - lookup_list = lookup_list[:-1] + [opts.pk.name, 'exact'] - if len(lookup_list) == 1: - _throw_bad_kwarg_error(kwarg) - lookup_type = lookup_list.pop() - current_opts = opts # We'll be overwriting this, so keep a reference to the original opts. - current_table_alias = current_opts.db_table - param_required = False - while lookup_list or param_required: - table_count += 1 - try: - # "current" is a piece of the lookup list. For example, in - # choices.get_list(poll__sites__id__exact=5), lookup_list is - # ["polls", "sites", "id"], and the first current is "polls". - try: - current = lookup_list.pop(0) - except IndexError: - # If we're here, lookup_list is empty but param_required - # is set to True, which means the kwarg was bad. - # Example: choices.get_list(poll__exact='foo') - _throw_bad_kwarg_error(kwarg) - # Try many-to-many relationships first... - for f in current_opts.many_to_many: - if f.name == current: - rel_table_alias = db.db.quote_name('t%s' % table_count) - table_count += 1 - tables.append('%s %s' % \ - (db.db.quote_name(f.get_m2m_db_table(current_opts)), rel_table_alias)) - join_where.append('%s.%s = %s.%s' % \ - (db.db.quote_name(current_table_alias), - db.db.quote_name(current_opts.pk.column), - rel_table_alias, - db.db.quote_name(current_opts.object_name.lower() + '_id'))) - # Optimization: In the case of primary-key lookups, we - # don't have to do an extra join. - if lookup_list and lookup_list[0] == f.rel.to.pk.name and lookup_type == 'exact': - where.append(_get_where_clause(lookup_type, rel_table_alias+'.', - f.rel.to.object_name.lower()+'_id', kwarg_value)) - params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) - lookup_list.pop() - param_required = False - else: - new_table_alias = 't%s' % table_count - tables.append('%s %s' % (db.db.quote_name(f.rel.to.db_table), - db.db.quote_name(new_table_alias))) - join_where.append('%s.%s = %s.%s' % \ - (db.db.quote_name(rel_table_alias), - db.db.quote_name(f.rel.to.object_name.lower() + '_id'), - db.db.quote_name(new_table_alias), - db.db.quote_name(f.rel.to.pk.column))) - current_table_alias = new_table_alias - param_required = True - current_opts = f.rel.to - raise StopIteration - for f in current_opts.fields: - # Try many-to-one relationships... - if f.rel and f.name == current: - # Optimization: In the case of primary-key lookups, we - # don't have to do an extra join. - if lookup_list and lookup_list[0] == f.rel.to.pk.name and lookup_type == 'exact': - where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value)) - params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) - lookup_list.pop() - param_required = False - # 'isnull' lookups in many-to-one relationships are a special case, - # because we don't want to do a join. We just want to find out - # whether the foreign key field is NULL. - elif lookup_type == 'isnull' and not lookup_list: - where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value)) - params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) - else: - new_table_alias = 't%s' % table_count - tables.append('%s %s' % \ - (db.db.quote_name(f.rel.to.db_table), db.db.quote_name(new_table_alias))) - join_where.append('%s.%s = %s.%s' % \ - (db.db.quote_name(current_table_alias), db.db.quote_name(f.column), - db.db.quote_name(new_table_alias), db.db.quote_name(f.rel.to.pk.column))) - current_table_alias = new_table_alias - param_required = True - current_opts = f.rel.to - raise StopIteration - # Try direct field-name lookups... - if f.name == current: - where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value)) - params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) - param_required = False - raise StopIteration - # If we haven't hit StopIteration at this point, "current" must be - # an invalid lookup, so raise an exception. - _throw_bad_kwarg_error(kwarg) - except StopIteration: - continue - return tables, join_where, where, params, table_count - -def function_get_sql_clause(opts, **kwargs): - def quote_only_if_word(word): - if ' ' in word: - return word - else: - return db.db.quote_name(word) - - # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. - select = ["%s.%s" % (db.db.quote_name(opts.db_table), db.db.quote_name(f.column)) for f in opts.fields] - tables = [opts.db_table] + (kwargs.get('tables') and kwargs['tables'][:] or []) - tables = [quote_only_if_word(t) for t in tables] - where = kwargs.get('where') and kwargs['where'][:] or [] - params = kwargs.get('params') and kwargs['params'][:] or [] - - # Convert the kwargs into SQL. - tables2, join_where2, where2, params2, _ = _parse_lookup(kwargs.items(), opts) - tables.extend(tables2) - where.extend(join_where2 + where2) - params.extend(params2) - - # Add any additional constraints from the "where_constraints" parameter. - where.extend(opts.where_constraints) - - # Add additional tables and WHERE clauses based on select_related. - if kwargs.get('select_related') is True: - _fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table]) - - # Add any additional SELECTs passed in via kwargs. - if kwargs.get('select'): - select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), db.db.quote_name(s[0])) for s in kwargs['select']]) - - # ORDER BY clause - order_by = [] - for f in handle_legacy_orderlist(kwargs.get('order_by', opts.ordering)): - if f == '?': # Special case. - order_by.append(db.get_random_function_sql()) - else: - if f.startswith('-'): - col_name = f[1:] - order = "DESC" - else: - col_name = f - order = "ASC" - if "." in col_name: - table_prefix, col_name = col_name.split('.', 1) - table_prefix = db.db.quote_name(table_prefix) + '.' - else: - # Use the database table as a column prefix if it wasn't given, - # and if the requested column isn't a custom SELECT. - if "." not in col_name and col_name not in [k[0] for k in kwargs.get('select', [])]: - table_prefix = db.db.quote_name(opts.db_table) + '.' - else: - table_prefix = '' - order_by.append('%s%s %s' % (table_prefix, db.db.quote_name(orderfield2column(col_name, opts)), order)) - order_by = ", ".join(order_by) - - # LIMIT and OFFSET clauses - if kwargs.get('limit') is not None: - limit_sql = " %s " % db.get_limit_offset_sql(kwargs['limit'], kwargs.get('offset')) - else: - assert kwargs.get('offset') is None, "'offset' is not allowed without 'limit'" - limit_sql = "" - - return select, " FROM " + ",".join(tables) + (where and " WHERE " + " AND ".join(where) or "") + (order_by and " ORDER BY " + order_by or "") + limit_sql, params - -def function_get_in_bulk(opts, klass, *args, **kwargs): - id_list = args and args[0] or kwargs.get('id_list', []) - assert id_list != [], "get_in_bulk() cannot be passed an empty list." - kwargs['where'] = ["%s.%s IN (%s)" % (db.db.quote_name(opts.db_table), db.db.quote_name(opts.pk.column), ",".join(['%s'] * len(id_list)))] - kwargs['params'] = id_list - obj_list = function_get_list(opts, klass, **kwargs) - return dict([(getattr(o, opts.pk.attname), o) for o in obj_list]) - -def function_get_latest(opts, klass, does_not_exist_exception, **kwargs): - kwargs['order_by'] = ('-' + opts.get_latest_by,) - kwargs['limit'] = 1 - return function_get_object(opts, klass, does_not_exist_exception, **kwargs) - -def function_get_date_list(opts, field, *args, **kwargs): - from django.core.db.typecasts import typecast_timestamp - kind = args and args[0] or kwargs['kind'] - assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'." - order = kwargs.pop('_order', 'ASC') - assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'" - kwargs['order_by'] = [] # Clear this because it'll mess things up otherwise. - if field.null: - kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % \ - (db.db.quote_name(opts.db_table), db.db.quote_name(field.column))) - select, sql, params = function_get_sql_clause(opts, **kwargs) - sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1' % (db.get_date_trunc_sql(kind, '%s.%s' % (db.db.quote_name(opts.db_table), db.db.quote_name(field.column))), sql) - cursor = db.db.cursor() - cursor.execute(sql, params) - # We have to manually run typecast_timestamp(str()) on the results, because - # MySQL doesn't automatically cast the result of date functions as datetime - # objects -- MySQL returns the values as strings, instead. - return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()] - -################################### -# HELPER FUNCTIONS (MANIPULATORS) # -################################### - -def get_manipulator(opts, klass, extra_methods, add=False, change=False): - "Returns the custom Manipulator (either add or change) for the given opts." - assert (add == False or change == False) and add != change, "get_manipulator() can be passed add=True or change=True, but not both" - man = types.ClassType('%sManipulator%s' % (opts.object_name, add and 'Add' or 'Change'), (formfields.Manipulator,), {}) - man.__module__ = MODEL_PREFIX + '.' + opts.module_name # Set this explicitly, as above. - man.__init__ = curry(manipulator_init, opts, add, change) - man.save = curry(manipulator_save, opts, klass, add, change) - man.get_related_objects = curry(manipulator_get_related_objects, opts, klass, add, change) - man.flatten_data = curry(manipulator_flatten_data, opts, klass, add, change) - for field_name_list in opts.unique_together: - setattr(man, 'isUnique%s' % '_'.join(field_name_list), curry(manipulator_validator_unique_together, field_name_list, opts)) - for f in opts.fields: - if f.unique_for_date: - setattr(man, 'isUnique%sFor%s' % (f.name, f.unique_for_date), curry(manipulator_validator_unique_for_date, f, opts.get_field(f.unique_for_date), opts, 'date')) - if f.unique_for_month: - setattr(man, 'isUnique%sFor%s' % (f.name, f.unique_for_month), curry(manipulator_validator_unique_for_date, f, opts.get_field(f.unique_for_month), opts, 'month')) - if f.unique_for_year: - setattr(man, 'isUnique%sFor%s' % (f.name, f.unique_for_year), curry(manipulator_validator_unique_for_date, f, opts.get_field(f.unique_for_year), opts, 'year')) - for k, v in extra_methods.items(): - setattr(man, k, v) - return man - -def manipulator_init(opts, add, change, self, obj_key=None, follow=None): - self.follow = opts.get_follow(follow) - - if change: - assert obj_key is not None, "ChangeManipulator.__init__() must be passed obj_key parameter." - self.obj_key = obj_key - try: - self.original_object = opts.get_model_module().get_object(pk=obj_key) - except ObjectDoesNotExist: - # If the object doesn't exist, this might be a manipulator for a - # one-to-one related object that hasn't created its subobject yet. - # For example, this might be a Restaurant for a Place that doesn't - # yet have restaurant information. - if opts.one_to_one_field: - # Sanity check -- Make sure the "parent" object exists. - # For example, make sure the Place exists for the Restaurant. - # Let the ObjectDoesNotExist exception propagate up. - lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to - lookup_kwargs['%s__exact' % opts.one_to_one_field.rel.field_name] = obj_key - _ = opts.one_to_one_field.rel.to.get_model_module().get_object(**lookup_kwargs) - params = dict([(f.attname, f.get_default()) for f in opts.fields]) - params[opts.pk.attname] = obj_key - self.original_object = opts.get_model_module().Klass(**params) - else: - raise - self.fields = [] - - for f in opts.fields + opts.many_to_many: - if self.follow.get(f.name, False): - self.fields.extend(f.get_manipulator_fields(opts, self, change)) - - # Add fields for related objects. - for f in opts.get_all_related_objects(): - if self.follow.get(f.name, False): - fol = self.follow[f.name] - self.fields.extend(f.get_manipulator_fields(opts, self, change, fol)) - - # Add field for ordering. - if change and opts.get_ordered_objects(): - self.fields.append(formfields.CommaSeparatedIntegerField(field_name="order_")) - -def manipulator_save(opts, klass, add, change, self, new_data): - # TODO: big cleanup when core fields go -> use recursive manipulators. - from django.utils.datastructures import DotExpandedDict - params = {} - for f in opts.fields: - # Fields with auto_now_add should keep their original value in the change stage. - auto_now_add = change and getattr(f, 'auto_now_add', False) - if self.follow.get(f.name, None) and not auto_now_add: - param = f.get_manipulator_new_data(new_data) - else: - if change: - param = getattr(self.original_object, f.attname) - else: - param = f.get_default() - params[f.attname] = param - - if change: - params[opts.pk.attname] = self.obj_key - - # First, save the basic object itself. - new_object = klass(**params) - new_object.save() - - # Now that the object's been saved, save any uploaded files. - for f in opts.fields: - if isinstance(f, FileField): - f.save_file(new_data, new_object, change and self.original_object or None, change, rel=False) - - # Calculate which primary fields have changed. - if change: - self.fields_added, self.fields_changed, self.fields_deleted = [], [], [] - for f in opts.fields: - if not f.primary_key and str(getattr(self.original_object, f.attname)) != str(getattr(new_object, f.attname)): - self.fields_changed.append(f.verbose_name) - - # Save many-to-many objects. Example: Poll.set_sites() - for f in opts.many_to_many: - if self.follow.get(f.name, None): - if not f.rel.edit_inline: - if f.rel.raw_id_admin: - new_vals = new_data.get(f.name, ()) - else: - new_vals = new_data.getlist(f.name) - was_changed = getattr(new_object, 'set_%s' % f.name)(new_vals) - if change and was_changed: - self.fields_changed.append(f.verbose_name) - - expanded_data = DotExpandedDict(dict(new_data)) - # Save many-to-one objects. Example: Add the Choice objects for a Poll. - for related in opts.get_all_related_objects(): - # Create obj_list, which is a DotExpandedDict such as this: - # [('0', {'id': ['940'], 'choice': ['This is the first choice']}), - # ('1', {'id': ['941'], 'choice': ['This is the second choice']}), - # ('2', {'id': [''], 'choice': ['']})] - child_follow = self.follow.get(related.name, None) - - if child_follow: - obj_list = expanded_data[related.var_name].items() - obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0]))) - - # For each related item... - for _, rel_new_data in obj_list: - - params = {} - - # Keep track of which core=True fields were provided. - # If all core fields were given, the related object will be saved. - # If none of the core fields were given, the object will be deleted. - # If some, but not all, of the fields were given, the validator would - # have caught that. - all_cores_given, all_cores_blank = True, True - - # Get a reference to the old object. We'll use it to compare the - # old to the new, to see which fields have changed. - old_rel_obj = None - if change: - if rel_new_data[related.opts.pk.name][0]: - try: - old_rel_obj = getattr(self.original_object, 'get_%s' % related.get_method_name_part() )(**{'%s__exact' % related.opts.pk.name: rel_new_data[related.opts.pk.attname][0]}) - except ObjectDoesNotExist: - pass - - for f in related.opts.fields: - if f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) in (None, ''): - all_cores_given = False - elif f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) not in (None, ''): - all_cores_blank = False - # If this field isn't editable, give it the same value it had - # previously, according to the given ID. If the ID wasn't - # given, use a default value. FileFields are also a special - # case, because they'll be dealt with later. - - if f == related.field: - param = getattr(new_object, related.field.rel.field_name) - elif add and isinstance(f, AutoField): - param = None - elif change and (isinstance(f, FileField) or not child_follow.get(f.name, None)): - if old_rel_obj: - param = getattr(old_rel_obj, f.column) - else: - param = f.get_default() - else: - param = f.get_manipulator_new_data(rel_new_data, rel=True) - if param != None: - params[f.attname] = param - - # Related links are a special case, because we have to - # manually set the "content_type_id" and "object_id" fields. - if opts.has_related_links and related.opts.module_name == 'relatedlinks': - contenttypes_mod = get_module('core', 'contenttypes') - params['content_type_id'] = contenttypes_mod.get_object(package__label__exact=opts.app_label, python_module_name__exact=opts.module_name).id - params['object_id'] = new_object.id - - # Create the related item. - new_rel_obj = related.opts.get_model_module().Klass(**params) - - # If all the core fields were provided (non-empty), save the item. - if all_cores_given: - new_rel_obj.save() - - # Save any uploaded files. - for f in related.opts.fields: - if child_follow.get(f.name, None): - if isinstance(f, FileField) and rel_new_data.get(f.name, False): - f.save_file(rel_new_data, new_rel_obj, change and old_rel_obj or None, old_rel_obj is not None, rel=True) - - # Calculate whether any fields have changed. - if change: - if not old_rel_obj: # This object didn't exist before. - self.fields_added.append('%s "%s"' % (related.opts.verbose_name, new_rel_obj)) - else: - for f in related.opts.fields: - if not f.primary_key and f != related.field and str(getattr(old_rel_obj, f.attname)) != str(getattr(new_rel_obj, f.attname)): - self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj)) - - # Save many-to-many objects. - for f in related.opts.many_to_many: - if child_follow.get(f.name, None) and not f.rel.edit_inline: - was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.attname]) - if change and was_changed: - self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj)) - - # If, in the change stage, all of the core fields were blank and - # the primary key (ID) was provided, delete the item. - if change and all_cores_blank and old_rel_obj: - new_rel_obj.delete() - self.fields_deleted.append('%s "%s"' % (related.opts.verbose_name, old_rel_obj)) - - # Save the order, if applicable. - if change and opts.get_ordered_objects(): - order = new_data['order_'] and map(int, new_data['order_'].split(',')) or [] - for rel_opts in opts.get_ordered_objects(): - getattr(new_object, 'set_%s_order' % rel_opts.object_name.lower())(order) - return new_object - -def manipulator_get_related_objects(opts, klass, add, change, self): - return opts.get_followed_related_objects(self.follow) - -def manipulator_flatten_data(opts, klass, add, change, self): - new_data = {} - obj = change and self.original_object or None - for f in opts.get_data_holders(self.follow): - fol = self.follow.get(f.name) - new_data.update(f.flatten_data(fol, obj)) - return new_data - -def manipulator_validator_unique_together(field_name_list, opts, self, field_data, all_data): - from django.utils.text import get_text_list - field_list = [opts.get_field(field_name) for field_name in field_name_list] - if isinstance(field_list[0].rel, ManyToOneRel): - kwargs = {'%s__%s__iexact' % (field_name_list[0], field_list[0].rel.field_name): field_data} - else: - kwargs = {'%s__iexact' % field_name_list[0]: field_data} - for f in field_list[1:]: - # This is really not going to work for fields that have different - # form fields, e.g. DateTime. - # This validation needs to occur after html2python to be effective. - field_val = all_data.get(f.attname, None) - if field_val is None: - # This will be caught by another validator, assuming the field - # doesn't have blank=True. - return - if isinstance(f.rel, ManyToOneRel): - kwargs['%s__pk' % f.name] = field_val - else: - kwargs['%s__iexact' % f.name] = field_val - mod = opts.get_model_module() - try: - old_obj = mod.get_object(**kwargs) - except ObjectDoesNotExist: - return - if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.attname) == getattr(old_obj, opts.pk.attname): - pass - else: - raise validators.ValidationError, _("%(object)s with this %(type)s already exists for the given %(field)s.") % { - 'object': capfirst(opts.verbose_name), 'type': field_list[0].verbose_name, 'field': get_text_list(field_name_list[1:], 'and')} - -def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_type, self, field_data, all_data): - date_str = all_data.get(date_field.get_manipulator_field_names('')[0], None) - mod = opts.get_model_module() - date_val = formfields.DateField.html2python(date_str) - if date_val is None: - return # Date was invalid. This will be caught by another validator. - lookup_kwargs = {'%s__year' % date_field.name: date_val.year} - if isinstance(from_field.rel, ManyToOneRel): - lookup_kwargs['%s__pk' % from_field.name] = field_data - else: - lookup_kwargs['%s__iexact' % from_field.name] = field_data - if lookup_type in ('month', 'date'): - lookup_kwargs['%s__month' % date_field.name] = date_val.month - if lookup_type == 'date': - lookup_kwargs['%s__day' % date_field.name] = date_val.day - try: - old_obj = mod.get_object(**lookup_kwargs) - except ObjectDoesNotExist: - return - else: - if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.attname) == getattr(old_obj, opts.pk.attname): - pass - else: - format_string = (lookup_type == 'date') and '%B %d, %Y' or '%B %Y' - raise validators.ValidationError, "Please enter a different %s. The one you entered is already being used for %s." % \ - (from_field.verbose_name, date_val.strftime(format_string)) diff --git a/django/core/paginator.py b/django/core/paginator.py index a4dbfebaae..6e01c1ccec 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -6,20 +6,17 @@ class InvalidPage(Exception): class ObjectPaginator: """ - This class makes pagination easy. Feed it a module (an object with - get_count() and get_list() methods) and a dictionary of arguments - to be passed to those methods, plus the number of objects you want - on each page. Then read the hits and pages properties to see how - many pages it involves. Call get_page with a page number (starting + This class makes pagination easy. Feed it a QuerySet, plus the number of + objects you want on each page. Then read the hits and pages properties to + see how many pages it involves. Call get_page with a page number (starting at 0) to get back a list of objects for that page. Finally, check if a page number has a next/prev page using has_next_page(page_number) and has_previous_page(page_number). """ - def __init__(self, module, args, num_per_page, count_method='get_count', list_method='get_list'): - self.module, self.args = module, args + def __init__(self, query_set, num_per_page): + self.query_set = query_set self.num_per_page = num_per_page - self.count_method, self.list_method = count_method, list_method self._hits, self._pages = None, None self._has_next = {} # Caches page_number -> has_next_boolean @@ -30,14 +27,17 @@ class ObjectPaginator: raise InvalidPage if page_number < 0: raise InvalidPage - args = copy(self.args) - args['offset'] = page_number * self.num_per_page + # Retrieve one extra record, and check for the existence of that extra # record to determine whether there's a next page. - args['limit'] = self.num_per_page + 1 - object_list = getattr(self.module, self.list_method)(**args) + limit = self.num_per_page + 1 + offset = page_number * self.num_per_page + + object_list = list(self.query_set[offset:offset+limit]) + if not object_list: raise InvalidPage + self._has_next[page_number] = (len(object_list) > self.num_per_page) return object_list[:self.num_per_page] @@ -45,11 +45,8 @@ class ObjectPaginator: "Does page $page_number have a 'next' page?" if not self._has_next.has_key(page_number): if self._pages is None: - args = copy(self.args) - args['offset'] = (page_number + 1) * self.num_per_page - args['limit'] = 1 - object_list = getattr(self.module, self.list_method)(**args) - self._has_next[page_number] = (object_list != []) + offset = (page_number + 1) * self.num_per_page + self._has_next[page_number] = len(self.query_set[offset:offset+1]) > 0 else: self._has_next[page_number] = page_number < (self.pages - 1) return self._has_next[page_number] @@ -59,12 +56,7 @@ class ObjectPaginator: def _get_hits(self): if self._hits is None: - order_args = copy(self.args) - if order_args.has_key('order_by'): - del order_args['order_by'] - if order_args.has_key('select_related'): - del order_args['select_related'] - self._hits = getattr(self.module, self.count_method)(**order_args) + self._hits = self.query_set.count() return self._hits def _get_pages(self): diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index c792e0a970..5772912031 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -242,7 +242,7 @@ class ServerHandler: # Error handling (also per-subclass or per-instance) traceback_limit = None # Print entire traceback to self.get_stderr() - error_status = "500 Dude, this is whack!" + error_status = "500 INTERNAL SERVER ERROR" error_headers = [('Content-Type','text/plain')] # State variables (don't mess with these) @@ -383,7 +383,7 @@ class ServerHandler: assert type(data) is StringType,"write() argument must be string" if not self.status: - raise AssertionError("write() before start_response()") + raise AssertionError("write() before start_response()") elif not self.headers_sent: # Before the first output, send the stored headers @@ -532,8 +532,8 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): server_version = "WSGIServer/" + __version__ def __init__(self, *args, **kwargs): - from django.conf.settings import ADMIN_MEDIA_PREFIX - self.admin_media_prefix = ADMIN_MEDIA_PREFIX + from django.conf import settings + self.admin_media_prefix = settings.ADMIN_MEDIA_PREFIX BaseHTTPRequestHandler.__init__(self, *args, **kwargs) def get_environ(self): diff --git a/django/core/signals.py b/django/core/signals.py new file mode 100644 index 0000000000..7a236079a5 --- /dev/null +++ b/django/core/signals.py @@ -0,0 +1,3 @@ +request_started = object() +request_finished = object() +got_request_exception = object() diff --git a/django/core/template_loader.py b/django/core/template_loader.py index e268c390e1..ee86178cc1 100644 --- a/django/core/template_loader.py +++ b/django/core/template_loader.py @@ -1,7 +1,7 @@ # This module is DEPRECATED! # -# You should no longer be using django.core.template_loader. +# You should no longer be using django.template_loader. # -# Use django.core.template.loader instead. +# Use django.template.loader instead. -from django.core.template.loader import * +from django.template.loader import * diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 9582dd4d81..e11b63e977 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -7,7 +7,8 @@ a string) and returns a tuple in this format: (view_function, function_args, function_kwargs) """ -from django.core.exceptions import Http404, ImproperlyConfigured, ViewDoesNotExist +from django.http import Http404 +from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist import re class Resolver404(Http404): diff --git a/django/core/validators.py b/django/core/validators.py index 88cf6db4e1..91d72033de 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -8,6 +8,9 @@ validator will *always* be run, regardless of whether its associated form field is required. """ +from django.conf import settings +from django.utils.translation import gettext, gettext_lazy, ngettext +from django.utils.functional import Promise, lazy import re _datere = r'(19|2\d)\d{2}-((?:0?[1-9])|(?:1[0-2]))-((?:0?[1-9])|(?:[12][0-9])|(?:3[0-1]))' @@ -24,10 +27,6 @@ phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNO slug_re = re.compile(r'^[-\w]+$') url_re = re.compile(r'^https?://\S+$') -from django.conf.settings import JING_PATH -from django.utils.translation import gettext_lazy, ngettext -from django.utils.functional import Promise, lazy - lazy_inter = lazy(lambda a,b: str(a) % b, str) class ValidationError(Exception): @@ -58,11 +57,11 @@ class CriticalValidationError(Exception): def isAlphaNumeric(field_data, all_data): if not alnum_re.search(field_data): - raise ValidationError, _("This value must contain only letters, numbers and underscores.") + raise ValidationError, gettext("This value must contain only letters, numbers and underscores.") def isAlphaNumericURL(field_data, all_data): if not alnumurl_re.search(field_data): - raise ValidationError, _("This value must contain only letters, numbers, underscores, dashes or slashes.") + raise ValidationError, gettext("This value must contain only letters, numbers, underscores, dashes or slashes.") def isSlug(field_data, all_data): if not slug_re.search(field_data): @@ -70,18 +69,18 @@ def isSlug(field_data, all_data): def isLowerCase(field_data, all_data): if field_data.lower() != field_data: - raise ValidationError, _("Uppercase letters are not allowed here.") + raise ValidationError, gettext("Uppercase letters are not allowed here.") def isUpperCase(field_data, all_data): if field_data.upper() != field_data: - raise ValidationError, _("Lowercase letters are not allowed here.") + raise ValidationError, gettext("Lowercase letters are not allowed here.") def isCommaSeparatedIntegerList(field_data, all_data): for supposed_int in field_data.split(','): try: int(supposed_int) except ValueError: - raise ValidationError, _("Enter only digits separated by commas.") + raise ValidationError, gettext("Enter only digits separated by commas.") def isCommaSeparatedEmailList(field_data, all_data): """ @@ -93,48 +92,48 @@ def isCommaSeparatedEmailList(field_data, all_data): try: isValidEmail(supposed_email.strip(), '') except ValidationError: - raise ValidationError, _("Enter valid e-mail addresses separated by commas.") + raise ValidationError, gettext("Enter valid e-mail addresses separated by commas.") def isValidIPAddress4(field_data, all_data): if not ip4_re.search(field_data): - raise ValidationError, _("Please enter a valid IP address.") + raise ValidationError, gettext("Please enter a valid IP address.") def isNotEmpty(field_data, all_data): if field_data.strip() == '': - raise ValidationError, _("Empty values are not allowed here.") + raise ValidationError, gettext("Empty values are not allowed here.") def isOnlyDigits(field_data, all_data): if not field_data.isdigit(): - raise ValidationError, _("Non-numeric characters aren't allowed here.") + raise ValidationError, gettext("Non-numeric characters aren't allowed here.") def isNotOnlyDigits(field_data, all_data): if field_data.isdigit(): - raise ValidationError, _("This value can't be comprised solely of digits.") + raise ValidationError, gettext("This value can't be comprised solely of digits.") def isInteger(field_data, all_data): # This differs from isOnlyDigits because this accepts the negative sign if not integer_re.search(field_data): - raise ValidationError, _("Enter a whole number.") + raise ValidationError, gettext("Enter a whole number.") def isOnlyLetters(field_data, all_data): if not field_data.isalpha(): - raise ValidationError, _("Only alphabetical characters are allowed here.") + raise ValidationError, gettext("Only alphabetical characters are allowed here.") def isValidANSIDate(field_data, all_data): if not ansi_date_re.search(field_data): - raise ValidationError, _('Enter a valid date in YYYY-MM-DD format.') + raise ValidationError, gettext('Enter a valid date in YYYY-MM-DD format.') def isValidANSITime(field_data, all_data): if not ansi_time_re.search(field_data): - raise ValidationError, _('Enter a valid time in HH:MM format.') + raise ValidationError, gettext('Enter a valid time in HH:MM format.') def isValidANSIDatetime(field_data, all_data): if not ansi_datetime_re.search(field_data): - raise ValidationError, _('Enter a valid date/time in YYYY-MM-DD HH:MM format.') + raise ValidationError, gettext('Enter a valid date/time in YYYY-MM-DD HH:MM format.') def isValidEmail(field_data, all_data): if not email_re.search(field_data): - raise ValidationError, _('Enter a valid e-mail address.') + raise ValidationError, gettext('Enter a valid e-mail address.') def isValidImage(field_data, all_data): """ @@ -146,18 +145,18 @@ def isValidImage(field_data, all_data): try: Image.open(StringIO(field_data['content'])) except IOError: # Python Imaging Library doesn't recognize it as an image - raise ValidationError, _("Upload a valid image. The file you uploaded was either not an image or a corrupted image.") + raise ValidationError, gettext("Upload a valid image. The file you uploaded was either not an image or a corrupted image.") def isValidImageURL(field_data, all_data): uc = URLMimeTypeCheck(('image/jpeg', 'image/gif', 'image/png')) try: uc(field_data, all_data) except URLMimeTypeCheck.InvalidContentType: - raise ValidationError, _("The URL %s does not point to a valid image.") % field_data + raise ValidationError, gettext("The URL %s does not point to a valid image.") % field_data def isValidPhone(field_data, all_data): if not phone_re.search(field_data): - raise ValidationError, _('Phone numbers must be in XXX-XXX-XXXX format. "%s" is invalid.') % field_data + raise ValidationError, gettext('Phone numbers must be in XXX-XXX-XXXX format. "%s" is invalid.') % field_data def isValidQuicktimeVideoURL(field_data, all_data): "Checks that the given URL is a video that can be played by QuickTime (qt, mpeg)" @@ -165,11 +164,11 @@ def isValidQuicktimeVideoURL(field_data, all_data): try: uc(field_data, all_data) except URLMimeTypeCheck.InvalidContentType: - raise ValidationError, _("The URL %s does not point to a valid QuickTime video.") % field_data + raise ValidationError, gettext("The URL %s does not point to a valid QuickTime video.") % field_data def isValidURL(field_data, all_data): if not url_re.search(field_data): - raise ValidationError, _("A valid URL is required.") + raise ValidationError, gettext("A valid URL is required.") def isValidHTML(field_data, all_data): import urllib, urllib2 @@ -183,14 +182,14 @@ def isValidHTML(field_data, all_data): return from xml.dom.minidom import parseString error_messages = [e.firstChild.wholeText for e in parseString(u.read()).getElementsByTagName('messages')[0].getElementsByTagName('msg')] - raise ValidationError, _("Valid HTML is required. Specific errors are:\n%s") % "\n".join(error_messages) + raise ValidationError, gettext("Valid HTML is required. Specific errors are:\n%s") % "\n".join(error_messages) def isWellFormedXml(field_data, all_data): from xml.dom.minidom import parseString try: parseString(field_data) except Exception, e: # Naked except because we're not sure what will be thrown - raise ValidationError, _("Badly formed XML: %s") % str(e) + raise ValidationError, gettext("Badly formed XML: %s") % str(e) def isWellFormedXmlFragment(field_data, all_data): isWellFormedXml('<root>%s</root>' % field_data, all_data) @@ -200,19 +199,19 @@ def isExistingURL(field_data, all_data): try: u = urllib2.urlopen(field_data) except ValueError: - raise ValidationError, _("Invalid URL: %s") % field_data + raise ValidationError, gettext("Invalid URL: %s") % field_data except urllib2.HTTPError, e: # 401s are valid; they just mean authorization is required. if e.code not in ('401',): - raise ValidationError, _("The URL %s is a broken link.") % field_data + raise ValidationError, gettext("The URL %s is a broken link.") % field_data except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError, _("The URL %s is a broken link.") % field_data + raise ValidationError, gettext("The URL %s is a broken link.") % field_data def isValidUSState(field_data, all_data): "Checks that the given string is a valid two-letter U.S. state abbreviation" states = ['AA', 'AE', 'AK', 'AL', 'AP', 'AR', 'AS', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'FM', 'GA', 'GU', 'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MH', 'MI', 'MN', 'MO', 'MP', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'PR', 'PW', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VA', 'VI', 'VT', 'WA', 'WI', 'WV', 'WY'] if field_data.upper() not in states: - raise ValidationError, _("Enter a valid U.S. state abbreviation.") + raise ValidationError, gettext("Enter a valid U.S. state abbreviation.") def hasNoProfanities(field_data, all_data): """ @@ -334,7 +333,7 @@ class IsAPowerOf: from math import log val = log(int(field_data)) / log(self.power_of) if val != int(val): - raise ValidationError, _("This value must be a power of %s.") % self.power_of + raise ValidationError, gettext("This value must be a power of %s.") % self.power_of class IsValidFloat: def __init__(self, max_digits, decimal_places): @@ -345,9 +344,9 @@ class IsValidFloat: try: float(data) except ValueError: - raise ValidationError, _("Please enter a valid decimal number.") + raise ValidationError, gettext("Please enter a valid decimal number.") if len(data) > (self.max_digits + 1): - raise ValidationError, ngettext( "Please enter a valid decimal number with at most %s total digit.", + raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.", "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits if '.' in data and len(data.split('.')[1]) > self.decimal_places: raise ValidationError, ngettext("Please enter a valid decimal number with at most %s decimal place.", @@ -424,10 +423,10 @@ class URLMimeTypeCheck: try: info = urllib2.urlopen(field_data).info() except (urllib2.HTTPError, urllib2.URLError): - raise URLMimeTypeCheck.CouldNotRetrieve, _("Could not retrieve anything from %s.") % field_data + raise URLMimeTypeCheck.CouldNotRetrieve, gettext("Could not retrieve anything from %s.") % field_data content_type = info['content-type'] if content_type not in self.mime_type_list: - raise URLMimeTypeCheck.InvalidContentType, _("The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'.") % { + raise URLMimeTypeCheck.InvalidContentType, gettext("The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'.") % { 'url': field_data, 'contenttype': content_type} class RelaxNGCompact: @@ -447,9 +446,9 @@ class RelaxNGCompact: fp = open(filename, 'w') fp.write(field_data) fp.close() - if not os.path.exists(JING_PATH): - raise Exception, "%s not found!" % JING_PATH - p = os.popen('%s -c %s %s' % (JING_PATH, self.schema_path, filename)) + if not os.path.exists(settings.JING_PATH): + raise Exception, "%s not found!" % settings.JING_PATH + p = os.popen('%s -c %s %s' % (settings.JING_PATH, self.schema_path, filename)) errors = [line.strip() for line in p.readlines()] p.close() os.unlink(filename) diff --git a/django/core/xheaders.py b/django/core/xheaders.py index 98d2586b75..e173bcbca8 100644 --- a/django/core/xheaders.py +++ b/django/core/xheaders.py @@ -9,14 +9,13 @@ that custom headers are prefxed with "X-"). Next time you're at slashdot.org, watch out for X-Fry and X-Bender. :) """ -def populate_xheaders(request, response, package, python_module_name, object_id): +def populate_xheaders(request, response, model, object_id): """ Adds the "X-Object-Type" and "X-Object-Id" headers to the given - HttpResponse according to the given package, python_module_name and - object_id -- but only if the given HttpRequest object has an IP address - within the INTERNAL_IPS setting. + HttpResponse according to the given model and object_id -- but only if the + given HttpRequest object has an IP address within the INTERNAL_IPS setting. """ - from django.conf.settings import INTERNAL_IPS - if request.META.get('REMOTE_ADDR') in INTERNAL_IPS: - response['X-Object-Type'] = "%s.%s" % (package, python_module_name) + from django.conf import settings + if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: + response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.object_name.lower()) response['X-Object-Id'] = str(object_id) diff --git a/django/db/__init__.py b/django/db/__init__.py new file mode 100644 index 0000000000..317d6059bf --- /dev/null +++ b/django/db/__init__.py @@ -0,0 +1,45 @@ +from django.conf import settings +from django.core import signals +from django.dispatch import dispatcher + +__all__ = ('backend', 'connection', 'DatabaseError') + +if not settings.DATABASE_ENGINE: + settings.DATABASE_ENGINE = 'dummy' + +try: + backend = __import__('django.db.backends.%s.base' % settings.DATABASE_ENGINE, '', '', ['']) +except ImportError, e: + # The database backend wasn't found. Display a helpful error message + # listing all possible database backends. + from django.core.exceptions import ImproperlyConfigured + import os + backend_dir = os.path.join(__path__[0], 'backends') + available_backends = [f for f in os.listdir(backend_dir) if not f.startswith('_') and not f.startswith('.') and not f.endswith('.py') and not f.endswith('.pyc')] + available_backends.sort() + raise ImproperlyConfigured, "Could not load database backend: %s. Is your DATABASE_ENGINE setting (currently, %r) spelled correctly? Available options are: %s" % \ + (e, settings.DATABASE_ENGINE, ", ".join(map(repr, available_backends))) + +get_introspection_module = lambda: __import__('django.db.backends.%s.introspection' % settings.DATABASE_ENGINE, '', '', ['']) +get_creation_module = lambda: __import__('django.db.backends.%s.creation' % settings.DATABASE_ENGINE, '', '', ['']) +runshell = lambda: __import__('django.db.backends.%s.client' % settings.DATABASE_ENGINE, '', '', ['']).runshell() + +connection = backend.DatabaseWrapper() +DatabaseError = backend.DatabaseError + +# Register an event that closes the database connection +# when a Django request is finished. +dispatcher.connect(connection.close, signal=signals.request_finished) + +# Register an event that resets connection.queries +# when a Django request is started. +def reset_queries(): + connection.queries = [] +dispatcher.connect(reset_queries, signal=signals.request_started) + +# Register an event that rolls back the connection +# when a Django request has an exception. +def _rollback_on_exception(): + from django.db import transaction + transaction.rollback_unless_managed() +dispatcher.connect(_rollback_on_exception, signal=signals.got_request_exception) diff --git a/django/parts/__init__.py b/django/db/backends/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/parts/__init__.py +++ b/django/db/backends/__init__.py diff --git a/django/parts/auth/__init__.py b/django/db/backends/ado_mssql/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/parts/auth/__init__.py +++ b/django/db/backends/ado_mssql/__init__.py diff --git a/django/core/db/backends/ado_mssql.py b/django/db/backends/ado_mssql/base.py index 4afe0cef70..b43be1fa7a 100644 --- a/django/core/db/backends/ado_mssql.py +++ b/django/db/backends/ado_mssql/base.py @@ -4,8 +4,7 @@ ADO MSSQL database backend for Django. Requires adodbapi 2.0.1: http://adodbapi.sourceforge.net/ """ -from django.core.db import base -from django.core.db.dicthelpers import * +from django.db.backends import util import adodbapi as Database import datetime try: @@ -45,10 +44,10 @@ def variantToPython(variant, adType): Database.convertVariantToPython = variantToPython try: - # Only exists in python 2.4+ + # Only exists in Python 2.4+ from threading import local except ImportError: - # Import copy of _thread_local.py from python 2.4 + # Import copy of _thread_local.py from Python 2.4 from django.utils._threading_local import local class DatabaseWrapper(local): @@ -57,25 +56,25 @@ class DatabaseWrapper(local): self.queries = [] def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG + from django.conf import settings if self.connection is None: - if DATABASE_NAME == '' or DATABASE_USER == '': + if settings.DATABASE_NAME == '' or settings.DATABASE_USER == '': from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured, "You need to specify both DATABASE_NAME and DATABASE_USER in your Django settings file." - if not DATABASE_HOST: - DATABASE_HOST = "127.0.0.1" + if not settings.DATABASE_HOST: + settings.DATABASE_HOST = "127.0.0.1" # TODO: Handle DATABASE_PORT. - conn_string = "PROVIDER=SQLOLEDB;DATA SOURCE=%s;UID=%s;PWD=%s;DATABASE=%s" % (DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME) + conn_string = "PROVIDER=SQLOLEDB;DATA SOURCE=%s;UID=%s;PWD=%s;DATABASE=%s" % (settings.DATABASE_HOST, settings.DATABASE_USER, settings.DATABASE_PASSWORD, settings.DATABASE_NAME) self.connection = Database.connect(conn_string) cursor = self.connection.cursor() - if DEBUG: + if settings.DEBUG: return base.CursorDebugWrapper(cursor, self) return cursor - def commit(self): + def _commit(self): return self.connection.commit() - def rollback(self): + def _rollback(self): if self.connection: return self.connection.rollback() @@ -84,10 +83,16 @@ class DatabaseWrapper(local): self.connection.close() self.connection = None - def quote_name(self, name): - if name.startswith('[') and name.endswith(']'): - return name # Quoting once is enough. - return '[%s]' % name +supports_constraints = True + +def quote_name(name): + if name.startswith('[') and name.endswith(']'): + return name # Quoting once is enough. + return '[%s]' % name + +dictfetchone = util.dictfetchone +dictfetchmany = util.dictfetchmany +dictfetchall = util.dictfetchall def get_last_insert_id(cursor, table_name, pk_name): cursor.execute("SELECT %s FROM %s WHERE %s = @@IDENTITY" % (pk_name, table_name, pk_name)) @@ -116,24 +121,14 @@ def get_limit_offset_sql(limit, offset=None): def get_random_function_sql(): return "RAND()" -def get_table_list(cursor): - raise NotImplementedError - -def get_table_description(cursor, table_name): - raise NotImplementedError - -def get_relations(cursor, table_name): - raise NotImplementedError - -def get_indexes(cursor, table_name): - raise NotImplementedError +def get_drop_foreignkey_sql(): + return "DROP CONSTRAINT" OPERATOR_MAPPING = { 'exact': '= %s', 'iexact': 'LIKE %s', 'contains': 'LIKE %s', 'icontains': 'LIKE %s', - 'ne': '!= %s', 'gt': '> %s', 'gte': '>= %s', 'lt': '< %s', @@ -143,32 +138,3 @@ OPERATOR_MAPPING = { 'istartswith': 'LIKE %s', 'iendswith': 'LIKE %s', } - -DATA_TYPES = { - 'AutoField': 'int IDENTITY (1, 1)', - 'BooleanField': 'bit', - 'CharField': 'varchar(%(maxlength)s)', - 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', - 'DateField': 'smalldatetime', - 'DateTimeField': 'smalldatetime', - 'FileField': 'varchar(100)', - 'FilePathField': 'varchar(100)', - 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'ImageField': 'varchar(100)', - 'IntegerField': 'int', - 'IPAddressField': 'char(15)', - 'ManyToManyField': None, - 'NullBooleanField': 'bit', - 'OneToOneField': 'int', - 'PhoneNumberField': 'varchar(20)', - 'PositiveIntegerField': 'int CONSTRAINT [CK_int_pos_%(column)s] CHECK ([%(column)s] > 0)', - 'PositiveSmallIntegerField': 'smallint CONSTRAINT [CK_smallint_pos_%(column)s] CHECK ([%(column)s] > 0)', - 'SlugField': 'varchar(%(maxlength)s)', - 'SmallIntegerField': 'smallint', - 'TextField': 'text', - 'TimeField': 'time', - 'URLField': 'varchar(200)', - 'USStateField': 'varchar(2)', -} - -DATA_TYPES_REVERSE = {} diff --git a/django/db/backends/ado_mssql/client.py b/django/db/backends/ado_mssql/client.py new file mode 100644 index 0000000000..5c197cafa4 --- /dev/null +++ b/django/db/backends/ado_mssql/client.py @@ -0,0 +1,2 @@ +def runshell(): + raise NotImplementedError diff --git a/django/db/backends/ado_mssql/creation.py b/django/db/backends/ado_mssql/creation.py new file mode 100644 index 0000000000..4d85d27ea5 --- /dev/null +++ b/django/db/backends/ado_mssql/creation.py @@ -0,0 +1,26 @@ +DATA_TYPES = { + 'AutoField': 'int IDENTITY (1, 1)', + 'BooleanField': 'bit', + 'CharField': 'varchar(%(maxlength)s)', + 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', + 'DateField': 'smalldatetime', + 'DateTimeField': 'smalldatetime', + 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', + 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'ImageField': 'varchar(100)', + 'IntegerField': 'int', + 'IPAddressField': 'char(15)', + 'ManyToManyField': None, + 'NullBooleanField': 'bit', + 'OneToOneField': 'int', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'int CONSTRAINT [CK_int_pos_%(column)s] CHECK ([%(column)s] > 0)', + 'PositiveSmallIntegerField': 'smallint CONSTRAINT [CK_smallint_pos_%(column)s] CHECK ([%(column)s] > 0)', + 'SlugField': 'varchar(%(maxlength)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'text', + 'TimeField': 'time', + 'URLField': 'varchar(200)', + 'USStateField': 'varchar(2)', +} diff --git a/django/db/backends/ado_mssql/introspection.py b/django/db/backends/ado_mssql/introspection.py new file mode 100644 index 0000000000..b125cc995f --- /dev/null +++ b/django/db/backends/ado_mssql/introspection.py @@ -0,0 +1,13 @@ +def get_table_list(cursor): + raise NotImplementedError + +def get_table_description(cursor, table_name): + raise NotImplementedError + +def get_relations(cursor, table_name): + raise NotImplementedError + +def get_indexes(cursor, table_name): + raise NotImplementedError + +DATA_TYPES_REVERSE = {} diff --git a/django/parts/media/__init__.py b/django/db/backends/dummy/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/parts/media/__init__.py +++ b/django/db/backends/dummy/__init__.py diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py new file mode 100644 index 0000000000..89fec00c1d --- /dev/null +++ b/django/db/backends/dummy/base.py @@ -0,0 +1,37 @@ +""" +Dummy database backend for Django. + +Django uses this if the DATABASE_ENGINE setting is empty (None or empty string). + +Each of these API functions, except connection.close(), raises +ImproperlyConfigured. +""" + +from django.core.exceptions import ImproperlyConfigured + +def complain(*args, **kwargs): + raise ImproperlyConfigured, "You haven't set the DATABASE_ENGINE setting yet." + +class DatabaseError(Exception): + pass + +class DatabaseWrapper: + cursor = complain + _commit = complain + _rollback = complain + + def close(self): + pass # close() + +supports_constraints = False +quote_name = complain +dictfetchone = complain +dictfetchmany = complain +dictfetchall = complain +get_last_insert_id = complain +get_date_extract_sql = complain +get_date_trunc_sql = complain +get_limit_offset_sql = complain +get_random_function_sql = complain +get_drop_foreignkey_sql = complain +OPERATOR_MAPPING = {} diff --git a/django/db/backends/dummy/client.py b/django/db/backends/dummy/client.py new file mode 100644 index 0000000000..e332987aa8 --- /dev/null +++ b/django/db/backends/dummy/client.py @@ -0,0 +1,3 @@ +from django.db.backends.dummy.base import complain + +runshell = complain diff --git a/django/db/backends/dummy/creation.py b/django/db/backends/dummy/creation.py new file mode 100644 index 0000000000..b82c4fe568 --- /dev/null +++ b/django/db/backends/dummy/creation.py @@ -0,0 +1 @@ +DATA_TYPES = {} diff --git a/django/db/backends/dummy/introspection.py b/django/db/backends/dummy/introspection.py new file mode 100644 index 0000000000..c52a812046 --- /dev/null +++ b/django/db/backends/dummy/introspection.py @@ -0,0 +1,8 @@ +from django.db.backends.dummy.base import complain + +get_table_list = complain +get_table_description = complain +get_relations = complain +get_indexes = complain + +DATA_TYPES_REVERSE = {} diff --git a/django/views/auth/__init__.py b/django/db/backends/mysql/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/views/auth/__init__.py +++ b/django/db/backends/mysql/__init__.py diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py new file mode 100644 index 0000000000..e2d6cbfc5e --- /dev/null +++ b/django/db/backends/mysql/base.py @@ -0,0 +1,167 @@ +""" +MySQL database backend for Django. + +Requires MySQLdb: http://sourceforge.net/projects/mysql-python +""" + +from django.db.backends import util +import MySQLdb as Database +from MySQLdb.converters import conversions +from MySQLdb.constants import FIELD_TYPE +import types + +DatabaseError = Database.DatabaseError + +django_conversions = conversions.copy() +django_conversions.update({ + types.BooleanType: util.rev_typecast_boolean, + FIELD_TYPE.DATETIME: util.typecast_timestamp, + FIELD_TYPE.DATE: util.typecast_date, + FIELD_TYPE.TIME: util.typecast_time, +}) + +# This is an extra debug layer over MySQL queries, to display warnings. +# It's only used when DEBUG=True. +class MysqlDebugWrapper: + def __init__(self, cursor): + self.cursor = cursor + + def execute(self, sql, params=()): + try: + return self.cursor.execute(sql, params) + except Database.Warning, w: + self.cursor.execute("SHOW WARNINGS") + raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) + + def executemany(self, sql, param_list): + try: + return self.cursor.executemany(sql, param_list) + except Database.Warning: + self.cursor.execute("SHOW WARNINGS") + raise Database.Warning, "%s: %s" % (w, self.cursor.fetchall()) + + def __getattr__(self, attr): + if self.__dict__.has_key(attr): + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + +try: + # Only exists in Python 2.4+ + from threading import local +except ImportError: + # Import copy of _thread_local.py from Python 2.4 + from django.utils._threading_local import local + +class DatabaseWrapper(local): + def __init__(self): + self.connection = None + self.queries = [] + + def _valid_connection(self): + if self.connection is not None: + try: + self.connection.ping() + return True + except DatabaseError: + self.connection.close() + self.connection = None + return False + + def cursor(self): + from django.conf import settings + if not self._valid_connection(): + kwargs = { + 'user': settings.DATABASE_USER, + 'db': settings.DATABASE_NAME, + 'passwd': settings.DATABASE_PASSWORD, + 'conv': django_conversions, + } + if settings.DATABASE_HOST.startswith('/'): + kwargs['unix_socket'] = settings.DATABASE_HOST + else: + kwargs['host'] = settings.DATABASE_HOST + if settings.DATABASE_PORT: + kwargs['port'] = int(settings.DATABASE_PORT) + self.connection = Database.connect(**kwargs) + cursor = self.connection.cursor() + if self.connection.get_server_info() >= '4.1': + cursor.execute("SET NAMES 'utf8'") + if settings.DEBUG: + return util.CursorDebugWrapper(MysqlDebugWrapper(cursor), self) + return cursor + + def _commit(self): + self.connection.commit() + + def _rollback(self): + if self.connection: + try: + self.connection.rollback() + except Database.NotSupportedError: + pass + + def close(self): + if self.connection is not None: + self.connection.close() + self.connection = None + +supports_constraints = True + +def quote_name(name): + if name.startswith("`") and name.endswith("`"): + return name # Quoting once is enough. + return "`%s`" % name + +dictfetchone = util.dictfetchone +dictfetchmany = util.dictfetchmany +dictfetchall = util.dictfetchall + +def get_last_insert_id(cursor, table_name, pk_name): + return cursor.lastrowid + +def get_date_extract_sql(lookup_type, table_name): + # lookup_type is 'year', 'month', 'day' + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), table_name) + +def get_date_trunc_sql(lookup_type, field_name): + # lookup_type is 'year', 'month', 'day' + fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] + format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. + format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') + try: + i = fields.index(lookup_type) + 1 + except ValueError: + sql = field_name + else: + format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) + sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) + return sql + +def get_limit_offset_sql(limit, offset=None): + sql = "LIMIT " + if offset and offset != 0: + sql += "%s," % offset + return sql + str(limit) + +def get_random_function_sql(): + return "RAND()" + +def get_drop_foreignkey_sql(): + return "DROP FOREIGN KEY" + +OPERATOR_MAPPING = { + 'exact': '= %s', + 'iexact': 'LIKE %s', + 'contains': 'LIKE BINARY %s', + 'icontains': 'LIKE %s', + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': 'LIKE BINARY %s', + 'endswith': 'LIKE BINARY %s', + 'istartswith': 'LIKE %s', + 'iendswith': 'LIKE %s', +} diff --git a/django/db/backends/mysql/client.py b/django/db/backends/mysql/client.py new file mode 100644 index 0000000000..f9d6297b8e --- /dev/null +++ b/django/db/backends/mysql/client.py @@ -0,0 +1,14 @@ +from django.conf import settings +import os + +def runshell(): + args = [''] + args += ["--user=%s" % settings.DATABASE_USER] + if settings.DATABASE_PASSWORD: + args += ["--password=%s" % settings.DATABASE_PASSWORD] + if settings.DATABASE_HOST: + args += ["--host=%s" % settings.DATABASE_HOST] + if settings.DATABASE_PORT: + args += ["--port=%s" % settings.DATABASE_PORT] + args += [settings.DATABASE_NAME] + os.execvp('mysql', args) diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py new file mode 100644 index 0000000000..116b490124 --- /dev/null +++ b/django/db/backends/mysql/creation.py @@ -0,0 +1,30 @@ +# This dictionary maps Field objects to their associated MySQL column +# types, as strings. Column-type strings can contain format strings; they'll +# be interpolated against the values of Field.__dict__ before being output. +# If a column type is set to None, it won't be included in the output. +DATA_TYPES = { + 'AutoField': 'integer AUTO_INCREMENT', + 'BooleanField': 'bool', + 'CharField': 'varchar(%(maxlength)s)', + 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', + 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'ImageField': 'varchar(100)', + 'IntegerField': 'integer', + 'IPAddressField': 'char(15)', + 'ManyToManyField': None, + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'integer UNSIGNED', + 'PositiveSmallIntegerField': 'smallint UNSIGNED', + 'SlugField': 'varchar(%(maxlength)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'longtext', + 'TimeField': 'time', + 'URLField': 'varchar(200)', + 'USStateField': 'varchar(2)', +} diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py new file mode 100644 index 0000000000..a2eeb6de7b --- /dev/null +++ b/django/db/backends/mysql/introspection.py @@ -0,0 +1,94 @@ +from django.db import transaction +from django.db.backends.mysql.base import quote_name +from MySQLdb import ProgrammingError, OperationalError +from MySQLdb.constants import FIELD_TYPE +import re + +foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") + +def get_table_list(cursor): + "Returns a list of table names in the current database." + cursor.execute("SHOW TABLES") + return [row[0] for row in cursor.fetchall()] + +def get_table_description(cursor, table_name): + "Returns a description of the table, with the DB-API cursor.description interface." + cursor.execute("SELECT * FROM %s LIMIT 1" % quote_name(table_name)) + return cursor.description + +def _name_to_index(cursor, table_name): + """ + Returns a dictionary of {field_name: field_index} for the given table. + Indexes are 0-based. + """ + return dict([(d[0], i) for i, d in enumerate(get_table_description(cursor, table_name))]) + +def get_relations(cursor, table_name): + """ + Returns a dictionary of {field_index: (field_index_other_table, other_table)} + representing all relationships to the given table. Indexes are 0-based. + """ + my_field_dict = _name_to_index(cursor, table_name) + constraints = [] + relations = {} + try: + # This should work for MySQL 5.0. + cursor.execute(""" + SELECT column_name, referenced_table_name, referenced_column_name + FROM information_schema.key_column_usage + WHERE table_name = %s + AND referenced_table_name IS NOT NULL + AND referenced_column_name IS NOT NULL""", [table_name]) + constraints.extend(cursor.fetchall()) + except (ProgrammingError, OperationalError): + # Fall back to "SHOW CREATE TABLE", for previous MySQL versions. + # Go through all constraints and save the equal matches. + cursor.execute("SHOW CREATE TABLE %s" % table_name) + for row in cursor.fetchall(): + pos = 0 + while True: + match = foreign_key_re.search(row[1], pos) + if match == None: + break + pos = match.end() + constraints.append(match.groups()) + + for my_fieldname, other_table, other_field in constraints: + other_field_index = _name_to_index(cursor, other_table)[other_field] + my_field_index = my_field_dict[my_fieldname] + relations[my_field_index] = (other_field_index, other_table) + + return relations + +def get_indexes(cursor, table_name): + """ + Returns a dictionary of fieldname -> infodict for the given table, + where each infodict is in the format: + {'primary_key': boolean representing whether it's the primary key, + 'unique': boolean representing whether it's a unique index} + """ + cursor.execute("SHOW INDEX FROM %s" % quote_name(table_name)) + indexes = {} + for row in cursor.fetchall(): + indexes[row[4]] = {'primary_key': (row[2] == 'PRIMARY'), 'unique': not bool(row[1])} + return indexes + +DATA_TYPES_REVERSE = { + FIELD_TYPE.BLOB: 'TextField', + FIELD_TYPE.CHAR: 'CharField', + FIELD_TYPE.DECIMAL: 'FloatField', + FIELD_TYPE.DATE: 'DateField', + FIELD_TYPE.DATETIME: 'DateTimeField', + FIELD_TYPE.DOUBLE: 'FloatField', + FIELD_TYPE.FLOAT: 'FloatField', + FIELD_TYPE.INT24: 'IntegerField', + FIELD_TYPE.LONG: 'IntegerField', + FIELD_TYPE.LONGLONG: 'IntegerField', + FIELD_TYPE.SHORT: 'IntegerField', + FIELD_TYPE.STRING: 'TextField', + FIELD_TYPE.TIMESTAMP: 'DateTimeField', + FIELD_TYPE.TINY_BLOB: 'TextField', + FIELD_TYPE.MEDIUM_BLOB: 'TextField', + FIELD_TYPE.LONG_BLOB: 'TextField', + FIELD_TYPE.VAR_STRING: 'CharField', +} diff --git a/django/views/registration/__init__.py b/django/db/backends/postgresql/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/views/registration/__init__.py +++ b/django/db/backends/postgresql/__init__.py diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py new file mode 100644 index 0000000000..3c807f2120 --- /dev/null +++ b/django/db/backends/postgresql/base.py @@ -0,0 +1,128 @@ +""" +PostgreSQL database backend for Django. + +Requires psycopg 1: http://initd.org/projects/psycopg1 +""" + +from django.db.backends import util +import psycopg as Database + +DatabaseError = Database.DatabaseError + +try: + # Only exists in Python 2.4+ + from threading import local +except ImportError: + # Import copy of _thread_local.py from Python 2.4 + from django.utils._threading_local import local + +class DatabaseWrapper(local): + def __init__(self): + self.connection = None + self.queries = [] + + def cursor(self): + from django.conf import settings + if self.connection is None: + if settings.DATABASE_NAME == '': + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured, "You need to specify DATABASE_NAME in your Django settings file." + conn_string = "dbname=%s" % settings.DATABASE_NAME + if settings.DATABASE_USER: + conn_string = "user=%s %s" % (settings.DATABASE_USER, conn_string) + if settings.DATABASE_PASSWORD: + conn_string += " password='%s'" % settings.DATABASE_PASSWORD + if settings.DATABASE_HOST: + conn_string += " host=%s" % settings.DATABASE_HOST + if settings.DATABASE_PORT: + conn_string += " port=%s" % settings.DATABASE_PORT + self.connection = Database.connect(conn_string) + self.connection.set_isolation_level(1) # make transactions transparent to all cursors + cursor = self.connection.cursor() + cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE]) + if settings.DEBUG: + return util.CursorDebugWrapper(cursor, self) + return cursor + + def _commit(self): + return self.connection.commit() + + def _rollback(self): + if self.connection: + return self.connection.rollback() + + def close(self): + if self.connection is not None: + self.connection.close() + self.connection = None + +supports_constraints = True + +def quote_name(name): + if name.startswith('"') and name.endswith('"'): + return name # Quoting once is enough. + return '"%s"' % name + +def dictfetchone(cursor): + "Returns a row from the cursor as a dict" + return cursor.dictfetchone() + +def dictfetchmany(cursor, number): + "Returns a certain number of rows from a cursor as a dict" + return cursor.dictfetchmany(number) + +def dictfetchall(cursor): + "Returns all rows from a cursor as a dict" + return cursor.dictfetchall() + +def get_last_insert_id(cursor, table_name, pk_name): + cursor.execute("SELECT CURRVAL('\"%s_%s_seq\"')" % (table_name, pk_name)) + return cursor.fetchone()[0] + +def get_date_extract_sql(lookup_type, table_name): + # lookup_type is 'year', 'month', 'day' + # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT + return "EXTRACT('%s' FROM %s)" % (lookup_type, table_name) + +def get_date_trunc_sql(lookup_type, field_name): + # lookup_type is 'year', 'month', 'day' + # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC + return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + +def get_limit_offset_sql(limit, offset=None): + sql = "LIMIT %s" % limit + if offset and offset != 0: + sql += " OFFSET %s" % offset + return sql + +def get_random_function_sql(): + return "RANDOM()" + +def get_drop_foreignkey_sql(): + return "DROP CONSTRAINT" + +# Register these custom typecasts, because Django expects dates/times to be +# in Python's native (standard-library) datetime/time format, whereas psycopg +# use mx.DateTime by default. +try: + Database.register_type(Database.new_type((1082,), "DATE", util.typecast_date)) +except AttributeError: + raise Exception, "You appear to be using psycopg version 2, which isn't supported yet, because it's still in beta. Use psycopg version 1 instead: http://initd.org/projects/psycopg1" +Database.register_type(Database.new_type((1083,1266), "TIME", util.typecast_time)) +Database.register_type(Database.new_type((1114,1184), "TIMESTAMP", util.typecast_timestamp)) +Database.register_type(Database.new_type((16,), "BOOLEAN", util.typecast_boolean)) + +OPERATOR_MAPPING = { + 'exact': '= %s', + 'iexact': 'ILIKE %s', + 'contains': 'LIKE %s', + 'icontains': 'ILIKE %s', + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': 'LIKE %s', + 'endswith': 'LIKE %s', + 'istartswith': 'ILIKE %s', + 'iendswith': 'ILIKE %s', +} diff --git a/django/db/backends/postgresql/client.py b/django/db/backends/postgresql/client.py new file mode 100644 index 0000000000..3d0d7a0d2a --- /dev/null +++ b/django/db/backends/postgresql/client.py @@ -0,0 +1,14 @@ +from django.conf import settings +import os + +def runshell(): + args = [''] + args += ["-U%s" % settings.DATABASE_USER] + if settings.DATABASE_PASSWORD: + args += ["-W"] + if settings.DATABASE_HOST: + args += ["-h %s" % settings.DATABASE_HOST] + if settings.DATABASE_PORT: + args += ["-p %s" % settings.DATABASE_PORT] + args += [settings.DATABASE_NAME] + os.execvp('psql', args) diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py new file mode 100644 index 0000000000..65a804ec40 --- /dev/null +++ b/django/db/backends/postgresql/creation.py @@ -0,0 +1,30 @@ +# This dictionary maps Field objects to their associated PostgreSQL column +# types, as strings. Column-type strings can contain format strings; they'll +# be interpolated against the values of Field.__dict__ before being output. +# If a column type is set to None, it won't be included in the output. +DATA_TYPES = { + 'AutoField': 'serial', + 'BooleanField': 'boolean', + 'CharField': 'varchar(%(maxlength)s)', + 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', + 'DateField': 'date', + 'DateTimeField': 'timestamp with time zone', + 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', + 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'ImageField': 'varchar(100)', + 'IntegerField': 'integer', + 'IPAddressField': 'inet', + 'ManyToManyField': None, + 'NullBooleanField': 'boolean', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)', + 'PositiveSmallIntegerField': 'smallint CHECK ("%(column)s" >= 0)', + 'SlugField': 'varchar(%(maxlength)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'text', + 'TimeField': 'time', + 'URLField': 'varchar(200)', + 'USStateField': 'varchar(2)', +} diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py new file mode 100644 index 0000000000..c4f759da10 --- /dev/null +++ b/django/db/backends/postgresql/introspection.py @@ -0,0 +1,85 @@ +from django.db import transaction +from django.db.backends.postgresql.base import quote_name + +def get_table_list(cursor): + "Returns a list of table names in the current database." + cursor.execute(""" + SELECT c.relname + FROM pg_catalog.pg_class c + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'v', '') + AND n.nspname NOT IN ('pg_catalog', 'pg_toast') + AND pg_catalog.pg_table_is_visible(c.oid)""") + return [row[0] for row in cursor.fetchall()] + +def get_table_description(cursor, table_name): + "Returns a description of the table, with the DB-API cursor.description interface." + cursor.execute("SELECT * FROM %s LIMIT 1" % quote_name(table_name)) + return cursor.description + +def get_relations(cursor, table_name): + """ + Returns a dictionary of {field_index: (field_index_other_table, other_table)} + representing all relationships to the given table. Indexes are 0-based. + """ + cursor.execute(""" + SELECT con.conkey, con.confkey, c2.relname + FROM pg_constraint con, pg_class c1, pg_class c2 + WHERE c1.oid = con.conrelid + AND c2.oid = con.confrelid + AND c1.relname = %s + AND con.contype = 'f'""", [table_name]) + relations = {} + for row in cursor.fetchall(): + try: + # row[0] and row[1] are like "{2}", so strip the curly braces. + relations[int(row[0][1:-1]) - 1] = (int(row[1][1:-1]) - 1, row[2]) + except ValueError: + continue + return relations + +def get_indexes(cursor, table_name): + """ + Returns a dictionary of fieldname -> infodict for the given table, + where each infodict is in the format: + {'primary_key': boolean representing whether it's the primary key, + 'unique': boolean representing whether it's a unique index} + """ + # Get the table description because we only have the column indexes, and we + # need the column names. + desc = get_table_description(cursor, table_name) + # This query retrieves each index on the given table. + cursor.execute(""" + SELECT idx.indkey, idx.indisunique, idx.indisprimary + FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, + pg_catalog.pg_index idx + WHERE c.oid = idx.indrelid + AND idx.indexrelid = c2.oid + AND c.relname = %s""", [table_name]) + indexes = {} + for row in cursor.fetchall(): + # row[0] (idx.indkey) is stored in the DB as an array. It comes out as + # a string of space-separated integers. This designates the field + # indexes (1-based) of the fields that have indexes on the table. + # Here, we skip any indexes across multiple fields. + if ' ' in row[0]: + continue + col_name = desc[int(row[0])-1][0] + indexes[col_name] = {'primary_key': row[2], 'unique': row[1]} + return indexes + +# Maps type codes to Django Field types. +DATA_TYPES_REVERSE = { + 16: 'BooleanField', + 21: 'SmallIntegerField', + 23: 'IntegerField', + 25: 'TextField', + 869: 'IPAddressField', + 1043: 'CharField', + 1082: 'DateField', + 1083: 'TimeField', + 1114: 'DateTimeField', + 1184: 'DateTimeField', + 1266: 'TimeField', + 1700: 'FloatField', +} diff --git a/django/db/backends/sqlite3/__init__.py b/django/db/backends/sqlite3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/db/backends/sqlite3/__init__.py diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py new file mode 100644 index 0000000000..ecaf9b0c0d --- /dev/null +++ b/django/db/backends/sqlite3/base.py @@ -0,0 +1,150 @@ +""" +SQLite3 backend for django. Requires pysqlite2 (http://pysqlite.org/). +""" + +from django.db.backends import util +from pysqlite2 import dbapi2 as Database + +DatabaseError = Database.DatabaseError + +Database.register_converter("bool", lambda s: str(s) == '1') +Database.register_converter("time", util.typecast_time) +Database.register_converter("date", util.typecast_date) +Database.register_converter("datetime", util.typecast_timestamp) + +def utf8rowFactory(cursor, row): + def utf8(s): + if type(s) == unicode: + return s.encode("utf-8") + else: + return s + return [utf8(r) for r in row] + +try: + # Only exists in Python 2.4+ + from threading import local +except ImportError: + # Import copy of _thread_local.py from Python 2.4 + from django.utils._threading_local import local + +class DatabaseWrapper(local): + def __init__(self): + self.connection = None + self.queries = [] + + def cursor(self): + from django.conf import settings + if self.connection is None: + self.connection = Database.connect(settings.DATABASE_NAME, detect_types=Database.PARSE_DECLTYPES) + # register extract and date_trun functions + self.connection.create_function("django_extract", 2, _sqlite_extract) + self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) + cursor = self.connection.cursor(factory=SQLiteCursorWrapper) + cursor.row_factory = utf8rowFactory + if settings.DEBUG: + return util.CursorDebugWrapper(cursor, self) + else: + return cursor + + def _commit(self): + self.connection.commit() + + def _rollback(self): + if self.connection: + self.connection.rollback() + + def close(self): + if self.connection is not None: + self.connection.close() + self.connection = None + +class SQLiteCursorWrapper(Database.Cursor): + """ + Django uses "format" style placeholders, but pysqlite2 uses "qmark" style. + This fixes it -- but note that if you want to use a literal "%s" in a query, + you'll need to use "%%s". + """ + def execute(self, query, params=()): + query = self.convert_query(query, len(params)) + return Database.Cursor.execute(self, query, params) + + def executemany(self, query, param_list): + query = self.convert_query(query, len(param_list[0])) + return Database.Cursor.executemany(self, query, param_list) + + def convert_query(self, query, num_params): + return query % tuple("?" * num_params) + +supports_constraints = False + +def quote_name(name): + if name.startswith('"') and name.endswith('"'): + return name # Quoting once is enough. + return '"%s"' % name + +dictfetchone = util.dictfetchone +dictfetchmany = util.dictfetchmany +dictfetchall = util.dictfetchall + +def get_last_insert_id(cursor, table_name, pk_name): + return cursor.lastrowid + +def get_date_extract_sql(lookup_type, table_name): + # lookup_type is 'year', 'month', 'day' + # sqlite doesn't support extract, so we fake it with the user-defined + # function _sqlite_extract that's registered in connect(), above. + return 'django_extract("%s", %s)' % (lookup_type.lower(), table_name) + +def _sqlite_extract(lookup_type, dt): + try: + dt = util.typecast_timestamp(dt) + except (ValueError, TypeError): + return None + return str(getattr(dt, lookup_type)) + +def get_date_trunc_sql(lookup_type, field_name): + # lookup_type is 'year', 'month', 'day' + # sqlite doesn't support DATE_TRUNC, so we fake it as above. + return 'django_date_trunc("%s", %s)' % (lookup_type.lower(), field_name) + +def get_limit_offset_sql(limit, offset=None): + sql = "LIMIT %s" % limit + if offset and offset != 0: + sql += " OFFSET %s" % offset + return sql + +def get_random_function_sql(): + return "RANDOM()" + +def get_drop_foreignkey_sql(): + return "" + +def _sqlite_date_trunc(lookup_type, dt): + try: + dt = util.typecast_timestamp(dt) + except (ValueError, TypeError): + return None + if lookup_type == 'year': + return "%i-01-01 00:00:00" % dt.year + elif lookup_type == 'month': + return "%i-%02i-01 00:00:00" % (dt.year, dt.month) + elif lookup_type == 'day': + return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) + +# SQLite requires LIKE statements to include an ESCAPE clause if the value +# being escaped has a percent or underscore in it. +# See http://www.sqlite.org/lang_expr.html for an explanation. +OPERATOR_MAPPING = { + 'exact': '= %s', + 'iexact': "LIKE %s ESCAPE '\\'", + 'contains': "LIKE %s ESCAPE '\\'", + 'icontains': "LIKE %s ESCAPE '\\'", + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': "LIKE %s ESCAPE '\\'", + 'endswith': "LIKE %s ESCAPE '\\'", + 'istartswith': "LIKE %s ESCAPE '\\'", + 'iendswith': "LIKE %s ESCAPE '\\'", +} diff --git a/django/db/backends/sqlite3/client.py b/django/db/backends/sqlite3/client.py new file mode 100644 index 0000000000..097218341f --- /dev/null +++ b/django/db/backends/sqlite3/client.py @@ -0,0 +1,6 @@ +from django.conf import settings +import os + +def runshell(): + args = ['', settings.DATABASE_NAME] + os.execvp('sqlite3', args) diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py new file mode 100644 index 0000000000..e845179e64 --- /dev/null +++ b/django/db/backends/sqlite3/creation.py @@ -0,0 +1,29 @@ +# SQLite doesn't actually support most of these types, but it "does the right +# thing" given more verbose field definitions, so leave them as is so that +# schema inspection is more useful. +DATA_TYPES = { + 'AutoField': 'integer', + 'BooleanField': 'bool', + 'CharField': 'varchar(%(maxlength)s)', + 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)', + 'DateField': 'date', + 'DateTimeField': 'datetime', + 'FileField': 'varchar(100)', + 'FilePathField': 'varchar(100)', + 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'ImageField': 'varchar(100)', + 'IntegerField': 'integer', + 'IPAddressField': 'char(15)', + 'ManyToManyField': None, + 'NullBooleanField': 'bool', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20)', + 'PositiveIntegerField': 'integer unsigned', + 'PositiveSmallIntegerField': 'smallint unsigned', + 'SlugField': 'varchar(%(maxlength)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'text', + 'TimeField': 'time', + 'URLField': 'varchar(200)', + 'USStateField': 'varchar(2)', +} diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py new file mode 100644 index 0000000000..c5fa738249 --- /dev/null +++ b/django/db/backends/sqlite3/introspection.py @@ -0,0 +1,50 @@ +from django.db import transaction +from django.db.backends.sqlite3.base import quote_name + +def get_table_list(cursor): + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name") + return [row[0] for row in cursor.fetchall()] + +def get_table_description(cursor, table_name): + cursor.execute("PRAGMA table_info(%s)" % quote_name(table_name)) + return [(row[1], row[2], None, None) for row in cursor.fetchall()] + +def get_relations(cursor, table_name): + raise NotImplementedError + +def get_indexes(cursor, table_name): + raise NotImplementedError + +# Maps SQL types to Django Field types. Some of the SQL types have multiple +# entries here because SQLite allows for anything and doesn't normalize the +# field type; it uses whatever was given. +BASE_DATA_TYPES_REVERSE = { + 'bool': 'BooleanField', + 'boolean': 'BooleanField', + 'smallint': 'SmallIntegerField', + 'smallinteger': 'SmallIntegerField', + 'int': 'IntegerField', + 'integer': 'IntegerField', + 'text': 'TextField', + 'char': 'CharField', + 'date': 'DateField', + 'datetime': 'DateTimeField', + 'time': 'TimeField', +} + +# This light wrapper "fakes" a dictionary interface, because some SQLite data +# types include variables in them -- e.g. "varchar(30)" -- and can't be matched +# as a simple dictionary lookup. +class FlexibleFieldLookupDict: + def __getitem__(self, key): + key = key.lower() + try: + return BASE_DATA_TYPES_REVERSE[key] + except KeyError: + import re + m = re.search(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$', key) + if m: + return ('CharField', {'maxlength': int(m.group(1))}) + raise KeyError + +DATA_TYPES_REVERSE = FlexibleFieldLookupDict() diff --git a/django/db/backends/util.py b/django/db/backends/util.py new file mode 100644 index 0000000000..b9c6f573c9 --- /dev/null +++ b/django/db/backends/util.py @@ -0,0 +1,114 @@ +import datetime +from time import time + +class CursorDebugWrapper: + def __init__(self, cursor, db): + self.cursor = cursor + self.db = db + + def execute(self, sql, params=()): + start = time() + try: + return self.cursor.execute(sql, params) + finally: + stop = time() + self.db.queries.append({ + 'sql': sql % tuple(params), + 'time': "%.3f" % (stop - start), + }) + + def executemany(self, sql, param_list): + start = time() + try: + return self.cursor.executemany(sql, param_list) + finally: + stop = time() + self.db.queries.append({ + 'sql': 'MANY: ' + sql + ' ' + str(tuple(param_list)), + 'time': "%.3f" % (stop - start), + }) + + def __getattr__(self, attr): + if self.__dict__.has_key(attr): + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + +############################################### +# Converters from database (string) to Python # +############################################### + +def typecast_date(s): + return s and datetime.date(*map(int, s.split('-'))) or None # returns None if s is null + +def typecast_time(s): # does NOT store time zone information + if not s: return None + hour, minutes, seconds = s.split(':') + if '.' in seconds: # check whether seconds have a fractional part + seconds, microseconds = seconds.split('.') + else: + microseconds = '0' + return datetime.time(int(hour), int(minutes), int(seconds), int(float('.'+microseconds) * 1000000)) + +def typecast_timestamp(s): # does NOT store time zone information + # "2005-07-29 15:48:00.590358-05" + # "2005-07-29 09:56:00-05" + if not s: return None + if not ' ' in s: return typecast_date(s) + d, t = s.split() + # Extract timezone information, if it exists. Currently we just throw + # it away, but in the future we may make use of it. + if '-' in t: + t, tz = t.split('-', 1) + tz = '-' + tz + elif '+' in t: + t, tz = t.split('+', 1) + tz = '+' + tz + else: + tz = '' + dates = d.split('-') + times = t.split(':') + seconds = times[2] + if '.' in seconds: # check whether seconds have a fractional part + seconds, microseconds = seconds.split('.') + else: + microseconds = '0' + return datetime.datetime(int(dates[0]), int(dates[1]), int(dates[2]), + int(times[0]), int(times[1]), int(seconds), int(float('.'+microseconds) * 1000000)) + +def typecast_boolean(s): + if s is None: return None + if not s: return False + return str(s)[0].lower() == 't' + +############################################### +# Converters from Python to database (string) # +############################################### + +def rev_typecast_boolean(obj, d): + return obj and '1' or '0' + +################################################################################## +# Helper functions for dictfetch* for databases that don't natively support them # +################################################################################## + +def _dict_helper(desc, row): + "Returns a dictionary for the given cursor.description and result row." + return dict([(desc[col[0]][0], col[1]) for col in enumerate(row)]) + +def dictfetchone(cursor): + "Returns a row from the cursor as a dict" + row = cursor.fetchone() + if not row: + return None + return _dict_helper(cursor.description, row) + +def dictfetchmany(cursor, number): + "Returns a certain number of rows from a cursor as a dict" + desc = cursor.description + return [_dict_helper(desc, row) for row in cursor.fetchmany(number)] + +def dictfetchall(cursor): + "Returns all rows from a cursor as a dict" + desc = cursor.description + return [_dict_helper(desc, row) for row in cursor.fetchall()] diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py new file mode 100644 index 0000000000..d708fa60bc --- /dev/null +++ b/django/db/models/__init__.py @@ -0,0 +1,40 @@ +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured +from django.core import validators +from django.db import backend, connection +from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models +from django.db.models.query import Q +from django.db.models.manager import Manager +from django.db.models.base import Model, AdminOptions +from django.db.models.fields import * +from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED +from django.db.models import signals +from django.utils.functional import curry +from django.utils.text import capfirst + +# Admin stages. +ADD, CHANGE, BOTH = 1, 2, 3 + +class LazyDate: + """ + Use in limit_choices_to to compare the field to dates calculated at run time + instead of when the model is loaded. For example:: + + ... limit_choices_to = {'date__gt' : models.LazyDate(days=-3)} ... + + which will limit the choices to dates greater than three days ago. + """ + def __init__(self, **kwargs): + self.delta = datetime.timedelta(**kwargs) + + def __str__(self): + return str(self.__get_value__()) + + def __repr__(self): + return "<LazyDate: %s>" % self.delta + + def __get_value__(self): + return datetime.datetime.now() + self.delta + + def __getattr__(self, attr): + return getattr(self.__get_value__(), attr) diff --git a/django/db/models/base.py b/django/db/models/base.py new file mode 100644 index 0000000000..2185471e2b --- /dev/null +++ b/django/db/models/base.py @@ -0,0 +1,401 @@ +import django.db.models.manipulators +import django.db.models.manager +from django.core import validators +from django.core.exceptions import ObjectDoesNotExist +from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist +from django.db.models.fields.related import OneToOneRel, ManyToOneRel +from django.db.models.related import RelatedObject +from django.db.models.query import orderlist2sql, delete_objects +from django.db.models.options import Options, AdminOptions +from django.db import connection, backend, transaction +from django.db.models import signals +from django.db.models.loading import register_models +from django.dispatch import dispatcher +from django.utils.datastructures import SortedDict +from django.utils.functional import curry +from django.conf import settings +import types +import sys +import os + +class ModelBase(type): + "Metaclass for all models" + def __new__(cls, name, bases, attrs): + # If this isn't a subclass of Model, don't do anything special. + if not bases or bases == (object,): + return type.__new__(cls, name, bases, attrs) + + # Create the class. + new_class = type.__new__(cls, name, bases, {'__module__': attrs.pop('__module__')}) + new_class.add_to_class('_meta', Options(attrs.pop('Meta', None))) + new_class.add_to_class('DoesNotExist', types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {})) + + # Build complete list of parents + for base in bases: + # TODO: Checking for the presence of '_meta' is hackish. + if '_meta' in dir(base): + new_class._meta.parents.append(base) + new_class._meta.parents.extend(base._meta.parents) + + model_module = sys.modules[new_class.__module__] + + if getattr(new_class._meta, 'app_label', None) is None: + # Figure out the app_label by looking one level up. + # For 'django.contrib.sites.models', this would be 'sites'. + new_class._meta.app_label = model_module.__name__.split('.')[-2] + + # Add all attributes to the class. + for obj_name, obj in attrs.items(): + new_class.add_to_class(obj_name, obj) + + # Add Fields inherited from parents + for parent in new_class._meta.parents: + for field in parent._meta.fields: + # Only add parent fields if they aren't defined for this class. + try: + new_class._meta.get_field(field.name) + except FieldDoesNotExist: + field.contribute_to_class(new_class, field.name) + + new_class._prepare() + + register_models(new_class._meta.app_label, new_class) + return new_class + +class Model(object): + __metaclass__ = ModelBase + + def _get_pk_val(self): + return getattr(self, self._meta.pk.attname) + + def __repr__(self): + return '<%s object>' % self.__class__.__name__ + + def __eq__(self, other): + return isinstance(other, self.__class__) and self._get_pk_val() == other._get_pk_val() + + def __ne__(self, other): + return not self.__eq__(other) + + def __init__(self, *args, **kwargs): + dispatcher.send(signal=signals.pre_init, sender=self.__class__, args=args, kwargs=kwargs) + for f in self._meta.fields: + if isinstance(f.rel, ManyToOneRel): + try: + # Assume object instance was passed in. + rel_obj = kwargs.pop(f.name) + except KeyError: + try: + # Object instance wasn't passed in -- must be an ID. + val = kwargs.pop(f.attname) + except KeyError: + val = f.get_default() + else: + # Object instance was passed in. + # Special case: You can pass in "None" for related objects if it's allowed. + if rel_obj is None and f.null: + val = None + else: + try: + val = getattr(rel_obj, f.rel.get_related_field().attname) + except AttributeError: + raise TypeError, "Invalid value: %r should be a %s instance, not a %s" % (f.name, f.rel.to, type(rel_obj)) + setattr(self, f.attname, val) + else: + val = kwargs.pop(f.attname, f.get_default()) + setattr(self, f.attname, val) + if kwargs: + raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] + for i, arg in enumerate(args): + setattr(self, self._meta.fields[i].attname, arg) + dispatcher.send(signal=signals.post_init, sender=self.__class__, instance=self) + + def add_to_class(cls, name, value): + if name == 'Admin': + assert type(value) == types.ClassType, "%r attribute of %s model must be a class, not a %s object" % (name, cls.__name__, type(value)) + value = AdminOptions(**dict([(k, v) for k, v in value.__dict__.items() if not k.startswith('_')])) + if hasattr(value, 'contribute_to_class'): + value.contribute_to_class(cls, name) + else: + setattr(cls, name, value) + add_to_class = classmethod(add_to_class) + + def _prepare(cls): + # Creates some methods once self._meta has been populated. + opts = cls._meta + opts._prepare(cls) + + if opts.order_with_respect_to: + cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) + cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) + setattr(opts.order_with_respect_to.rel.to, 'get_%s_order' % cls.__name__.lower(), curry(method_get_order, cls)) + setattr(opts.order_with_respect_to.rel.to, 'set_%s_order' % cls.__name__.lower(), curry(method_set_order, cls)) + + # Give the class a docstring -- its definition. + if cls.__doc__ is None: + cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join([f.attname for f in opts.fields])) + + if hasattr(cls, 'get_absolute_url'): + cls.get_absolute_url = curry(get_absolute_url, opts, cls.get_absolute_url) + + dispatcher.send(signal=signals.class_prepared, sender=cls) + + _prepare = classmethod(_prepare) + + def save(self): + dispatcher.send(signal=signals.pre_save, sender=self.__class__, instance=self) + + non_pks = [f for f in self._meta.fields if not f.primary_key] + cursor = connection.cursor() + + # First, try an UPDATE. If that doesn't update anything, do an INSERT. + pk_val = self._get_pk_val() + pk_set = bool(pk_val) + record_exists = True + if pk_set: + # Determine whether a record with the primary key already exists. + cursor.execute("SELECT 1 FROM %s WHERE %s=%%s LIMIT 1" % \ + (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column)), [pk_val]) + # If it does already exist, do an UPDATE. + if cursor.fetchone(): + db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), False)) for f in non_pks] + cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \ + (backend.quote_name(self._meta.db_table), + ','.join(['%s=%%s' % backend.quote_name(f.column) for f in non_pks]), + backend.quote_name(self._meta.pk.attname)), + db_values + [pk_val]) + else: + record_exists = False + if not pk_set or not record_exists: + field_names = [backend.quote_name(f.column) for f in self._meta.fields if not isinstance(f, AutoField)] + db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.attname), True)) for f in self._meta.fields if not isinstance(f, AutoField)] + # If the PK has been manually set, respect that. + if pk_set: + field_names += [f.column for f in self._meta.fields if isinstance(f, AutoField)] + db_values += [f.get_db_prep_save(f.pre_save(getattr(self, f.column), True)) for f in self._meta.fields if isinstance(f, AutoField)] + placeholders = ['%s'] * len(field_names) + if self._meta.order_with_respect_to: + field_names.append(backend.quote_name('_order')) + # TODO: This assumes the database supports subqueries. + placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \ + (backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.order_with_respect_to.column))) + db_values.append(getattr(self, self._meta.order_with_respect_to.attname)) + cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \ + (backend.quote_name(self._meta.db_table), ','.join(field_names), + ','.join(placeholders)), db_values) + if self._meta.has_auto_field and not pk_set: + setattr(self, self._meta.pk.attname, backend.get_last_insert_id(cursor, self._meta.db_table, self._meta.pk.column)) + transaction.commit_unless_managed() + + # Run any post-save hooks. + dispatcher.send(signal=signals.post_save, sender=self.__class__, instance=self) + + save.alters_data = True + + def validate(self): + """ + First coerces all fields on this instance to their proper Python types. + Then runs validation on every field. Returns a dictionary of + field_name -> error_list. + """ + error_dict = {} + invalid_python = {} + for f in self._meta.fields: + try: + setattr(self, f.attname, f.to_python(getattr(self, f.attname, f.get_default()))) + except validators.ValidationError, e: + error_dict[f.name] = e.messages + invalid_python[f.name] = 1 + for f in self._meta.fields: + if f.name in invalid_python: + continue + errors = f.validate_full(getattr(self, f.attname, f.get_default()), self.__dict__) + if errors: + error_dict[f.name] = errors + return error_dict + + def _collect_sub_objects(self, seen_objs): + """ + Recursively populates seen_objs with all objects related to this object. + When done, seen_objs will be in the format: + {model_class: {pk_val: obj, pk_val: obj, ...}, + model_class: {pk_val: obj, pk_val: obj, ...}, ...} + """ + pk_val = self._get_pk_val() + if pk_val in seen_objs.setdefault(self.__class__, {}): + return + seen_objs.setdefault(self.__class__, {})[pk_val] = self + + for related in self._meta.get_all_related_objects(): + rel_opts_name = related.get_accessor_name() + if isinstance(related.field.rel, OneToOneRel): + try: + sub_obj = getattr(self, rel_opts_name) + except ObjectDoesNotExist: + pass + else: + sub_obj._collect_sub_objects(seen_objs) + else: + for sub_obj in getattr(self, rel_opts_name).all(): + sub_obj._collect_sub_objects(seen_objs) + + def delete(self): + assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname) + + # Find all the objects than need to be deleted + seen_objs = SortedDict() + self._collect_sub_objects(seen_objs) + + # Actually delete the objects + delete_objects(seen_objs) + + delete.alters_data = True + + def _get_FIELD_display(self, field): + value = getattr(self, field.attname) + return dict(field.choices).get(value, value) + + def _get_next_or_previous_by_FIELD(self, field, is_next): + op = is_next and '>' or '<' + where = '(%s %s %%s OR (%s = %%s AND %s.%s %s %%s))' % \ + (backend.quote_name(field.column), op, backend.quote_name(field.column), + backend.quote_name(self._meta.db_table), backend.quote_name(self._meta.pk.column), op) + param = str(getattr(self, field.attname)) + q = self.__class__._default_manager.order_by((not is_next and '-' or '') + field.name, (not is_next and '-' or '') + self._meta.pk.name) + q._where.append(where) + q._params.extend([param, param, getattr(self, self._meta.pk.attname)]) + return q[0] + + def _get_next_or_previous_in_order(self, is_next): + cachename = "__%s_order_cache" % is_next + if not hasattr(self, cachename): + op = is_next and '>' or '<' + order_field = self._meta.order_with_respect_to + where = ['%s %s (SELECT %s FROM %s WHERE %s=%%s)' % \ + (backend.quote_name('_order'), op, backend.quote_name('_order'), + backend.quote_name(opts.db_table), backend.quote_name(opts.pk.column)), + '%s=%%s' % backend.quote_name(order_field.column)] + params = [self._get_pk_val(), getattr(self, order_field.attname)] + obj = self._default_manager.order_by('_order').extra(where=where, params=params)[:1].get() + setattr(self, cachename, obj) + return getattr(self, cachename) + + def _get_FIELD_filename(self, field): + return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)) + + def _get_FIELD_url(self, field): + if getattr(self, field.attname): # value is not blank + import urlparse + return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/') + return '' + + def _get_FIELD_size(self, field): + return os.path.getsize(self.__get_FIELD_filename(field)) + + def _save_FIELD_file(self, field, filename, raw_contents): + directory = field.get_directory_name() + try: # Create the date-based directory if it doesn't exist. + os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) + except OSError: # Directory probably already exists. + pass + filename = field.get_filename(filename) + + # If the filename already exists, keep adding an underscore to the name of + # the file until the filename doesn't exist. + while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)): + try: + dot_index = filename.rindex('.') + except ValueError: # filename has no dot + filename += '_' + else: + filename = filename[:dot_index] + '_' + filename[dot_index:] + + # Write the file to disk. + setattr(self, field.attname, filename) + + full_filename = self._get_FIELD_filename(field) + fp = open(full_filename, 'wb') + fp.write(raw_contents) + fp.close() + + # Save the width and/or height, if applicable. + if isinstance(field, ImageField) and (field.width_field or field.height_field): + from django.utils.images import get_image_dimensions + width, height = get_image_dimensions(full_filename) + if field.width_field: + setattr(self, field.width_field, width) + if field.height_field: + setattr(self, field.height_field, height) + + # Save the object, because it has changed. + self.save() + + _save_FIELD_file.alters_data = True + + def _get_FIELD_width(self, field): + return self.__get_image_dimensions(field)[0] + + def _get_FIELD_height(self, field): + return self.__get_image_dimensions(field)[1] + + def _get_image_dimensions(self, field): + cachename = "__%s_dimensions_cache" % field.name + if not hasattr(self, cachename): + from django.utils.images import get_image_dimensions + filename = self.__get_FIELD_filename(field)() + setattr(self, cachename, get_image_dimensions(filename)) + return getattr(self, cachename) + + # Handles setting many-to-many related objects. + # Example: Album.set_songs() + def _set_related_many_to_many(self, rel_class, rel_field, id_list): + id_list = map(int, id_list) # normalize to integers + rel = rel_field.rel.to + m2m_table = rel_field.m2m_db_table() + this_id = self._get_pk_val() + cursor = connection.cursor() + cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ + (backend.quote_name(m2m_table), + backend.quote_name(rel_field.m2m_column_name())), [this_id]) + sql = "INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ + (backend.quote_name(m2m_table), + backend.quote_name(rel_field.m2m_column_name()), + backend.quote_name(rel_field.m2m_reverse_name())) + cursor.executemany(sql, [(this_id, i) for i in id_list]) + transaction.commit_unless_managed() + +############################################ +# HELPER FUNCTIONS (CURRIED MODEL METHODS) # +############################################ + +# ORDERING METHODS ######################### + +def method_set_order(ordered_obj, self, id_list): + cursor = connection.cursor() + # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" + sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \ + (backend.quote_name(ordered_obj.db_table), backend.quote_name('_order'), + backend.quote_name(ordered_obj.order_with_respect_to.column), + backend.quote_name(ordered_obj.pk.column)) + rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) + cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) + transaction.commit_unless_managed() + +def method_get_order(ordered_obj, self): + cursor = connection.cursor() + # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" + sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY %s" % \ + (backend.quote_name(ordered_obj._meta.pk.column), + backend.quote_name(ordered_obj._meta.db_table), + backend.quote_name(ordered_obj._meta.order_with_respect_to.column), + backend.quote_name('_order')) + rel_val = getattr(self, ordered_obj._meta.order_with_respect_to.rel.field_name) + cursor.execute(sql, [rel_val]) + return [r[0] for r in cursor.fetchall()] + +############################################## +# HELPER FUNCTIONS (CURRIED MODEL FUNCTIONS) # +############################################## + +def get_absolute_url(opts, func, self): + return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self) diff --git a/django/core/meta/fields.py b/django/db/models/fields/__init__.py index 8eeb9550c6..8cc17079a9 100644 --- a/django/core/meta/fields.py +++ b/django/db/models/fields/__init__.py @@ -1,10 +1,13 @@ +from django.db.models import signals +from django.dispatch import dispatcher from django.conf import settings -from django.core import formfields, validators +from django.core import validators +from django import forms from django.core.exceptions import ObjectDoesNotExist from django.utils.functional import curry, lazy from django.utils.text import capfirst -from django.utils.translation import gettext_lazy, ngettext -import datetime, os +from django.utils.translation import gettext, gettext_lazy, ngettext +import datetime, os, time class NOT_PROVIDED: pass @@ -16,71 +19,29 @@ HORIZONTAL, VERTICAL = 1, 2 BLANK_CHOICE_DASH = [("", "---------")] BLANK_CHOICE_NONE = [("", "None")] -# Values for Relation.edit_inline. -TABULAR, STACKED = 1, 2 - -RECURSIVE_RELATIONSHIP_CONSTANT = 'self' - # prepares a value for use in a LIKE query prep_for_like_query = lambda x: str(x).replace("%", "\%").replace("_", "\_") # returns the <ul> class for a given radio_admin value get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') -def string_concat(*strings): - """" - lazy variant of string concatenation, needed for translations that are - constructed from multiple parts. Handles lazy strings and non-strings by - first turning all arguments to strings, before joining them. - """ - return ''.join([str(el) for el in strings]) - -string_concat = lazy(string_concat, str) - -def manipulator_valid_rel_key(f, self, field_data, all_data): - "Validates that the value is a valid foreign key" - mod = f.rel.to.get_model_module() - try: - mod.get_object(pk=field_data) - except ObjectDoesNotExist: - raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name +class FieldDoesNotExist(Exception): + pass def manipulator_validator_unique(f, opts, self, field_data, all_data): "Validates that the value is unique for this field." - if f.rel and isinstance(f.rel, ManyToOneRel): - lookup_type = '%s__%s__exact' % (f.name, f.rel.get_related_field().name) - else: - lookup_type = '%s__exact' % f.name + lookup_type = f.get_validator_unique_lookup_type() try: - old_obj = opts.get_model_module().get_object(**{lookup_type: field_data}) + old_obj = self.manager.get(**{lookup_type: field_data}) except ObjectDoesNotExist: return - if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.attname) == getattr(old_obj, opts.pk.attname): + if getattr(self, 'original_object', None) and self.original_object._get_pk_val() == old_obj._get_pk_val(): return - raise validators.ValidationError, _("%(optname)s with this %(fieldname)s already exists.") % {'optname': capfirst(opts.verbose_name), 'fieldname': f.verbose_name} - -class BoundField(object): - def __init__(self, field, field_mapping, original): - self.field = field - self.original = original - self.form_fields = self.resolve_form_fields(field_mapping) - - def resolve_form_fields(self, field_mapping): - return [field_mapping[name] for name in self.field.get_manipulator_field_names('')] - - def as_field_list(self): - return [self.field] - - def original_value(self): - if self.original: - return self.original.__dict__[self.field.column] - - def __repr__(self): - return "BoundField:(%s, %s)" % (self.field.name, self.form_fields) + raise validators.ValidationError, gettext("%(optname)s with this %(fieldname)s already exists.") % {'optname': capfirst(opts.verbose_name), 'fieldname': f.verbose_name} # A guide to Field parameters: # -# * name: The name of the field specified in the model. +# * name: The name of the field specifed in the model. # * attname: The attribute to use on the model object. This is the same as # "name", except in the case of ForeignKeys, where "_id" is # appended. @@ -103,13 +64,13 @@ class Field(object): creation_counter = 0 def __init__(self, verbose_name=None, name=None, primary_key=False, - maxlength=None, unique=False, blank=False, null=False, db_index=None, + maxlength=None, unique=False, blank=False, null=False, db_index=False, core=False, rel=None, default=NOT_PROVIDED, editable=True, prepopulate_from=None, unique_for_date=None, unique_for_month=None, unique_for_year=None, validator_list=None, choices=None, radio_admin=None, help_text='', db_column=None): self.name = name - self.verbose_name = verbose_name or (name and name.replace('_', ' ')) + self.verbose_name = verbose_name self.primary_key = primary_key self.maxlength, self.unique = maxlength, unique self.blank, self.null = blank, null @@ -123,39 +84,70 @@ class Field(object): self.radio_admin = radio_admin self.help_text = help_text self.db_column = db_column - if rel and isinstance(rel, ManyToManyRel): - if rel.raw_id_admin: - self.help_text = string_concat(self.help_text, - gettext_lazy(' Separate multiple IDs with commas.')) - else: - self.help_text = string_concat(self.help_text, - gettext_lazy(' Hold down "Control", or "Command" on a Mac, to select more than one.')) # Set db_index to True if the field has a relationship and doesn't explicitly set db_index. - if db_index is None: - if isinstance(rel, OneToOneRel) or isinstance(rel, ManyToOneRel): - self.db_index = True - else: - self.db_index = False - else: - self.db_index = db_index + self.db_index = db_index # Increase the creation counter, and save our local copy. self.creation_counter = Field.creation_counter Field.creation_counter += 1 + def __cmp__(self, other): + # This is needed because bisect does not take a comparison function. + return cmp(self.creation_counter, other.creation_counter) + + def to_python(self, value): + """ + Converts the input value into the expected Python data type, raising + validators.ValidationError if the data can't be converted. Returns the + converted value. Subclasses should override this. + """ + return value + + def validate_full(self, field_data, all_data): + """ + Returns a list of errors for this field. This is the main interface, + as it encapsulates some basic validation logic used by all fields. + Subclasses should implement validate(), not validate_full(). + """ + if not self.blank and not field_data: + return [gettext_lazy('This field is required.')] + try: + self.validate(field_data, all_data) + except validators.ValidationError, e: + return e.messages + return [] + + def validate(self, field_data, all_data): + """ + Raises validators.ValidationError if field_data has any errors. + Subclasses should override this to specify field-specific validation + logic. This method should assume field_data has already been converted + into the appropriate data type by Field.to_python(). + """ + pass + + def set_attributes_from_name(self, name): + self.name = name self.attname, self.column = self.get_attname_column() + self.verbose_name = self.verbose_name or (name and name.replace('_', ' ')) + + def contribute_to_class(self, cls, name): + self.set_attributes_from_name(name) + cls._meta.add_field(self) + if self.choices: + setattr(cls, 'get_%s_display' % self.name, curry(cls._get_FIELD_display, field=self)) def set_name(self, name): self.name = name self.verbose_name = self.verbose_name or name.replace('_', ' ') self.attname, self.column = self.get_attname_column() + def get_attname(self): + return self.name + def get_attname_column(self): - if isinstance(self.rel, ManyToOneRel): - attname = '%s_id' % self.name - else: - attname = self.name + attname = self.get_attname() column = self.db_column or attname return attname, column @@ -198,8 +190,8 @@ class Field(object): def get_default(self): "Returns the default value for this field." if self.default is not NOT_PROVIDED: - if hasattr(self.default, '__get_value__'): - return self.default.__get_value__() + if callable(self.default): + return self.default() return self.default if not self.empty_strings_allowed or self.null: return None @@ -211,42 +203,32 @@ class Field(object): """ return [name_prefix + self.name] - def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False): - """ - Returns a list of formfields.FormField instances for this field. It - calculates the choices at runtime, not at compile time. - - name_prefix is a prefix to prepend to the "field_name" argument. - rel is a boolean specifying whether this field is in a related context. - """ + def prepare_field_objs_and_params(self, manipulator, name_prefix): params = {'validator_list': self.validator_list[:]} if self.maxlength and not self.choices: # Don't give SelectFields a maxlength parameter. params['maxlength'] = self.maxlength - if isinstance(self.rel, ManyToOneRel): - params['member_name'] = name_prefix + self.attname - if self.rel.raw_id_admin: - field_objs = self.get_manipulator_field_objs() - params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) - else: - if self.radio_admin: - field_objs = [formfields.RadioSelectField] - params['ul_class'] = get_ul_class(self.radio_admin) - else: - if self.null: - field_objs = [formfields.NullSelectField] - else: - field_objs = [formfields.SelectField] - params['choices'] = self.get_choices_default() - elif self.choices: + + if self.choices: if self.radio_admin: - field_objs = [formfields.RadioSelectField] + field_objs = [forms.RadioSelectField] params['ul_class'] = get_ul_class(self.radio_admin) else: - field_objs = [formfields.SelectField] + field_objs = [forms.SelectField] params['choices'] = self.get_choices_default() else: field_objs = self.get_manipulator_field_objs() + return (field_objs, params) + + def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): + """ + Returns a list of forms.FormField instances for this field. It + calculates the choices at runtime, not at compile time. + + name_prefix is a prefix to prepend to the "field_name" argument. + rel is a boolean specifying whether this field is in a related context. + """ + field_objs, params = self.prepare_field_objs_and_params(manipulator, name_prefix) # Add the "unique" validator(s). for field_name_list in opts.unique_together: @@ -269,6 +251,11 @@ class Field(object): # RequiredIfOtherFieldGiven. params['is_required'] = not self.blank and not self.primary_key and not rel + # BooleanFields (CheckboxFields) are a special case. They don't take + # is_required or validator_list. + if isinstance(self, BooleanField): + del params['validator_list'], params['is_required'] + # If this field is in a related context, check whether any other fields # in the related object have core=True. If so, add a validator -- # RequiredIfOtherFieldsGiven -- to this FormField. @@ -282,15 +269,13 @@ class Field(object): if core_field_names: params['validator_list'].append(validators.RequiredIfOtherFieldsGiven(core_field_names, gettext_lazy("This field is required."))) - # BooleanFields (CheckboxFields) are a special case. They don't take - # is_required or validator_list. - if isinstance(self, BooleanField): - del params['validator_list'], params['is_required'] - # Finally, add the field_names. field_names = self.get_manipulator_field_names(name_prefix) return [man(field_name=field_names[i], **params) for i, man in enumerate(field_objs)] + def get_validator_unique_lookup_type(self): + return '%s__exact' % self.name + def get_manipulator_new_data(self, new_data, rel=False): """ Given the full new_data dictionary (from the manipulator), returns this @@ -298,32 +283,31 @@ class Field(object): """ if rel: return new_data.get(self.name, [self.get_default()])[0] - else: - val = new_data.get(self.name, self.get_default()) - if not self.empty_strings_allowed and val == '' and self.null: - val = None - return val + val = new_data.get(self.name, self.get_default()) + if not self.empty_strings_allowed and val == '' and self.null: + val = None + return val def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): "Returns a list of tuples used as SelectField choices for this field." first_choice = include_blank and blank_choice or [] if self.choices: return first_choice + list(self.choices) - rel_obj = self.rel.to - return first_choice + [(getattr(x, rel_obj.pk.attname), str(x)) - for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)] + rel_model = self.rel.to + return first_choice + [(x._get_pk_val(), str(x)) + for x in rel_model._default_manager.filter(**self.rel.limit_choices_to)] def get_choices_default(self): - if(self.radio_admin): + if self.radio_admin: return self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) else: return self.get_choices() def _get_val_from_obj(self, obj): if obj: - return getattr(obj, self.attname) + return getattr(obj, self.attname) else: - return self.get_default() + return self.get_default() def flatten_data(self, follow, obj=None): """ @@ -339,57 +323,101 @@ class Field(object): else: return self.editable - def bind(self, fieldmapping, original, bound_field_class=BoundField): + def bind(self, fieldmapping, original, bound_field_class): return bound_field_class(self, fieldmapping, original) class AutoField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__ + kwargs['blank'] = True Field.__init__(self, *args, **kwargs) - def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False): + def to_python(self, value): + if value is None: + return value + try: + return int(value) + except (TypeError, ValueError): + raise validators.ValidationError, gettext("This value must be an integer.") + + def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): if not rel: return [] # Don't add a FormField unless it's in a related context. - return Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel) + return Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow) def get_manipulator_field_objs(self): - return [formfields.HiddenField] + return [forms.HiddenField] def get_manipulator_new_data(self, new_data, rel=False): + # Never going to be called + # Not in main change pages + # ignored in related context if not rel: return None return Field.get_manipulator_new_data(self, new_data, rel) + def contribute_to_class(self, cls, name): + assert not cls._meta.has_auto_field, "A model can't have more than one AutoField." + super(AutoField, self).contribute_to_class(cls, name) + cls._meta.has_auto_field = True + class BooleanField(Field): def __init__(self, *args, **kwargs): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def to_python(self, value): + if value in (True, False): return value + if value is 't': return True + if value is 'f': return False + raise validators.ValidationError, gettext("This value must be either True or False.") + def get_manipulator_field_objs(self): - return [formfields.CheckboxField] + return [forms.CheckboxField] class CharField(Field): def get_manipulator_field_objs(self): - return [formfields.TextField] + return [forms.TextField] + def to_python(self, value): + if isinstance(value, basestring): + return value + if value is None: + if self.null: + return value + else: + raise validators.ValidationError, gettext_lazy("This field cannot be null.") + return str(value) + +# TODO: Maybe move this into contrib, because it's specialized. class CommaSeparatedIntegerField(CharField): def get_manipulator_field_objs(self): - return [formfields.CommaSeparatedIntegerField] + return [forms.CommaSeparatedIntegerField] class DateField(Field): empty_strings_allowed = False def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add - #HACKs : auto_now_add/auto_now should be done as a default or a pre_save... + #HACKs : auto_now_add/auto_now should be done as a default or a pre_save. if auto_now or auto_now_add: kwargs['editable'] = False kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def to_python(self, value): + if isinstance(value, datetime.datetime): + return value.date() + if isinstance(value, datetime.date): + return value + validators.isValidANSIDate(value, None) + return datetime.date(*time.strptime(value, '%Y-%m-%d')[:3]) + def get_db_prep_lookup(self, lookup_type, value): if lookup_type == 'range': value = [str(v) for v in value] + elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne'): + value = value.strftime('%Y-%m-%d') else: value = str(value) return Field.get_db_prep_lookup(self, lookup_type, value) @@ -399,6 +427,14 @@ class DateField(Field): return datetime.datetime.now() return value + def contribute_to_class(self, cls, name): + super(DateField,self).contribute_to_class(cls, name) + if not self.null: + setattr(cls, 'get_next_by_%s' % self.name, + curry(cls._get_next_or_previous_by_FIELD, field=self, is_next=True)) + setattr(cls, 'get_previous_by_%s' % self.name, + curry(cls._get_next_or_previous_by_FIELD, field=self, is_next=False)) + # Needed because of horrible auto_now[_add] behaviour wrt. editable def get_follow(self, override=None): if override != None: @@ -413,13 +449,29 @@ class DateField(Field): return Field.get_db_prep_save(self, value) def get_manipulator_field_objs(self): - return [formfields.DateField] + return [forms.DateField] def flatten_data(self, follow, obj = None): val = self._get_val_from_obj(obj) return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')} class DateTimeField(DateField): + def to_python(self, value): + if isinstance(value, datetime.datetime): + return value + if isinstance(value, datetime.date): + return datetime.datetime(value.year, value.month, value.day) + try: # Seconds are optional, so try converting seconds first. + return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M:%S')[:6]) + except ValueError: + try: # Try without seconds. + return datetime.datetime(*time.strptime(value, '%Y-%m-%d %H:%M')[:5]) + except ValueError: # Try without hour/minutes/seconds. + try: + return datetime.datetime(*time.strptime(value, '%Y-%m-%d')[:3]) + except ValueError: + raise validators.ValidationError, gettext('Enter a valid date/time in YYYY-MM-DD HH:MM format.') + def get_db_prep_save(self, value): # Casts dates into string format for entry into database. if value is not None: @@ -430,8 +482,15 @@ class DateTimeField(DateField): value = str(value) return Field.get_db_prep_save(self, value) + def get_db_prep_lookup(self, lookup_type, value): + if lookup_type == 'range': + value = [str(v) for v in value] + else: + value = str(value) + return Field.get_db_prep_lookup(self, lookup_type, value) + def get_manipulator_field_objs(self): - return [formfields.DateField, formfields.TimeField] + return [forms.DateField, forms.TimeField] def get_manipulator_field_names(self, name_prefix): return [name_prefix + self.name + '_date', name_prefix + self.name + '_time'] @@ -454,25 +513,27 @@ class DateTimeField(DateField): return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), time_field: (val is not None and val.strftime("%H:%M:%S") or '')} -class EmailField(Field): +class EmailField(CharField): def __init__(self, *args, **kwargs): kwargs['maxlength'] = 75 - Field.__init__(self, *args, **kwargs) + CharField.__init__(self, *args, **kwargs) def get_internal_type(self): return "CharField" def get_manipulator_field_objs(self): - return [formfields.EmailField] + return [forms.EmailField] + + def validate(self, field_data, all_data): + validators.isValidEmail(field_data, all_data) class FileField(Field): def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs): self.upload_to = upload_to Field.__init__(self, verbose_name, name, **kwargs) - def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False): - field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel) - + def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): + field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow) if not self.blank: if rel: # This validator makes sure FileFields work in a related context. @@ -507,8 +568,25 @@ class FileField(Field): field_list[1].validator_list.append(isWithinMediaRoot) return field_list + def contribute_to_class(self, cls, name): + super(FileField, self).contribute_to_class(cls, name) + setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self)) + setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self)) + setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self)) + setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents: instance._save_FIELD_file(self, filename, raw_contents)) + dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls) + + def delete_file(self, instance): + if getattr(instance, self.attname): + file_name = getattr(instance, 'get_%s_filename' % self.name)() + # If the file exists and no other object of this type references it, + # delete it from the filesystem. + if os.path.exists(file_name) and \ + not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}): + os.remove(file_name) + def get_manipulator_field_objs(self): - return [formfields.FileUploadField, formfields.HiddenField] + return [forms.FileUploadField, forms.HiddenField] def get_manipulator_field_names(self, name_prefix): return [name_prefix + self.name + '_file', name_prefix + self.name] @@ -536,7 +614,7 @@ class FilePathField(Field): Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [curry(formfields.FilePathField, path=self.path, match=self.match, recursive=self.recursive)] + return [curry(forms.FilePathField, path=self.path, match=self.match, recursive=self.recursive)] class FloatField(Field): empty_strings_allowed = False @@ -545,7 +623,7 @@ class FloatField(Field): Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [curry(formfields.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)] + return [curry(forms.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)] class ImageField(FileField): def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): @@ -553,7 +631,16 @@ class ImageField(FileField): FileField.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [formfields.ImageUploadField, formfields.HiddenField] + return [forms.ImageUploadField, forms.HiddenField] + + def contribute_to_class(self, cls, name): + super(ImageField, self).contribute_to_class(cls, name) + # Add get_BLAH_width and get_BLAH_height methods, but only if the + # image field doesn't have width and height cache fields. + if not self.width_field: + setattr(cls, 'get_%s_width' % self.name, curry(cls._get_FIELD_width, field=self)) + if not self.height_field: + setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self)) def save_file(self, new_data, new_object, original_object, change, rel): FileField.save_file(self, new_data, new_object, original_object, change, rel) @@ -570,7 +657,7 @@ class ImageField(FileField): class IntegerField(Field): empty_strings_allowed = False def get_manipulator_field_objs(self): - return [formfields.IntegerField] + return [forms.IntegerField] class IPAddressField(Field): def __init__(self, *args, **kwargs): @@ -578,7 +665,10 @@ class IPAddressField(Field): Field.__init__(self, *args, **kwargs) def get_manipulator_field_objs(self): - return [formfields.IPAddressField] + return [forms.IPAddressField] + + def validate(self, field_data, all_data): + validators.isValidIPAddress4(field_data, None) class NullBooleanField(Field): def __init__(self, *args, **kwargs): @@ -586,23 +676,25 @@ class NullBooleanField(Field): Field.__init__(self, *args, **kwargs) def get_manipulator_field_objs(self): - return [formfields.NullBooleanField] + return [forms.NullBooleanField] class PhoneNumberField(IntegerField): def get_manipulator_field_objs(self): - return [formfields.PhoneNumberField] + return [forms.PhoneNumberField] + + def validate(self, field_data, all_data): + validators.isValidPhone(field_data, all_data) class PositiveIntegerField(IntegerField): def get_manipulator_field_objs(self): - return [formfields.PositiveIntegerField] + return [forms.PositiveIntegerField] class PositiveSmallIntegerField(IntegerField): def get_manipulator_field_objs(self): - return [formfields.PositiveSmallIntegerField] + return [forms.PositiveSmallIntegerField] class SlugField(Field): def __init__(self, *args, **kwargs): - # Default to a maxlength of 50 but allow overrides. kwargs['maxlength'] = kwargs.get('maxlength', 50) kwargs.setdefault('validator_list', []).append(validators.isSlug) # Set db_index=True unless it's been set manually. @@ -611,20 +703,20 @@ class SlugField(Field): Field.__init__(self, *args, **kwargs) def get_manipulator_field_objs(self): - return [formfields.TextField] + return [forms.TextField] class SmallIntegerField(IntegerField): def get_manipulator_field_objs(self): - return [formfields.SmallIntegerField] + return [forms.SmallIntegerField] class TextField(Field): def get_manipulator_field_objs(self): - return [formfields.LargeTextField] + return [forms.LargeTextField] class TimeField(Field): empty_strings_allowed = False def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): - self.auto_now, self.auto_now_add = auto_now, auto_now_add + self.auto_now, self.auto_now_add = auto_now, auto_now_add if auto_now or auto_now_add: kwargs['editable'] = False Field.__init__(self, verbose_name, name, **kwargs) @@ -652,7 +744,7 @@ class TimeField(Field): return Field.get_db_prep_save(self, value) def get_manipulator_field_objs(self): - return [formfields.TimeField] + return [forms.TimeField] def flatten_data(self,follow, obj = None): val = self._get_val_from_obj(obj) @@ -665,11 +757,11 @@ class URLField(Field): Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): - return [formfields.URLField] + return [forms.URLField] class USStateField(Field): def get_manipulator_field_objs(self): - return [formfields.USStateField] + return [forms.USStateField] class XMLField(TextField): def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs): @@ -680,277 +772,17 @@ class XMLField(TextField): return "TextField" def get_manipulator_field_objs(self): - return [curry(formfields.XMLLargeTextField, schema_path=self.schema_path)] - -class ForeignKey(Field): - empty_strings_allowed = False - def __init__(self, to, to_field=None, **kwargs): - try: - to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert to == 'self', "ForeignKey(%r) is invalid. First parameter to ForeignKey must be either a model or the string %r" % (to, RECURSIVE_RELATIONSHIP_CONSTANT) - else: - to_field = to_field or to._meta.pk.name - kwargs['verbose_name'] = kwargs.get('verbose_name', '') - - if kwargs.has_key('edit_inline_type'): - import warnings - warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") - kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - - kwargs['rel'] = ManyToOneRel(to, to_field, - num_in_admin=kwargs.pop('num_in_admin', 3), - min_num_in_admin=kwargs.pop('min_num_in_admin', None), - max_num_in_admin=kwargs.pop('max_num_in_admin', None), - num_extra_on_change=kwargs.pop('num_extra_on_change', 1), - edit_inline=kwargs.pop('edit_inline', False), - related_name=kwargs.pop('related_name', None), - limit_choices_to=kwargs.pop('limit_choices_to', None), - lookup_overrides=kwargs.pop('lookup_overrides', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) - Field.__init__(self, **kwargs) - - def get_manipulator_field_objs(self): - rel_field = self.rel.get_related_field() - if self.rel.raw_id_admin and not isinstance(rel_field, AutoField): - return rel_field.get_manipulator_field_objs() - else: - return [formfields.IntegerField] - - def get_db_prep_save(self,value): - if value == '' or value == None: - return None - else: - return self.rel.get_related_field().get_db_prep_save(value) - - def flatten_data(self, follow, obj=None): - if not obj: - # In required many-to-one fields with only one available choice, - # select that one available choice. Note: For SelectFields - # (radio_admin=False), we have to check that the length of choices - # is *2*, not 1, because SelectFields always have an initial - # "blank" value. Otherwise (radio_admin=True), we check that the - # length is 1. - if not self.blank and (not self.rel.raw_id_admin or self.choices): - choice_list = self.get_choices_default() - if self.radio_admin and len(choice_list) == 1: - return {self.attname: choice_list[0][0]} - if not self.radio_admin and len(choice_list) == 2: - return {self.attname: choice_list[1][0]} - return Field.flatten_data(self, follow, obj) - -class ManyToManyField(Field): - def __init__(self, to, **kwargs): - kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name_plural) - kwargs['rel'] = ManyToManyRel(to, kwargs.pop('singular', None), - num_in_admin=kwargs.pop('num_in_admin', 0), - related_name=kwargs.pop('related_name', None), - filter_interface=kwargs.pop('filter_interface', None), - limit_choices_to=kwargs.pop('limit_choices_to', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) - if kwargs["rel"].raw_id_admin: - kwargs.setdefault("validator_list", []).append(self.isValidIDList) - Field.__init__(self, **kwargs) - - def get_manipulator_field_objs(self): - if self.rel.raw_id_admin: - return [formfields.RawIdAdminField] - else: - choices = self.get_choices_default() - return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] - - def get_choices_default(self): - return Field.get_choices(self, include_blank=False) - - def get_m2m_db_table(self, original_opts): - "Returns the name of the many-to-many 'join' table." - return '%s_%s' % (original_opts.db_table, self.name) - - def isValidIDList(self, field_data, all_data): - "Validates that the value is a valid list of foreign keys" - mod = self.rel.to.get_model_module() - try: - pks = map(int, field_data.split(',')) - except ValueError: - # the CommaSeparatedIntegerField validator will catch this error - return - objects = mod.get_in_bulk(pks) - if len(objects) != len(pks): - badkeys = [k for k in pks if k not in objects] - raise validators.ValidationError, ngettext("Please enter valid %(self)s IDs. The value %(value)r is invalid.", - "Please enter valid %(self)s IDs. The values %(value)r are invalid.", len(badkeys)) % { - 'self': self.verbose_name, - 'value': len(badkeys) == 1 and badkeys[0] or tuple(badkeys), - } - - def flatten_data(self, follow, obj = None): - new_data = {} - if obj: - get_list_func = getattr(obj, 'get_%s_list' % self.rel.singular) - instance_ids = [getattr(instance, self.rel.to.pk.attname) for instance in get_list_func()] - if self.rel.raw_id_admin: - new_data[self.name] = ",".join([str(id) for id in instance_ids]) - else: - new_data[self.name] = instance_ids - else: - # In required many-to-many fields with only one available choice, - # select that one available choice. - if not self.blank and not self.rel.edit_inline and not self.rel.raw_id_admin: - choices_list = self.get_choices_default() - if len(choices_list) == 1: - new_data[self.name] = [choices_list[0][0]] - return new_data - -class OneToOneField(IntegerField): - def __init__(self, to, to_field=None, **kwargs): - kwargs['verbose_name'] = kwargs.get('verbose_name', 'ID') - to_field = to_field or to._meta.pk.name - - if kwargs.has_key('edit_inline_type'): - import warnings - warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") - kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - - kwargs['rel'] = OneToOneRel(to, to_field, - num_in_admin=kwargs.pop('num_in_admin', 0), - edit_inline=kwargs.pop('edit_inline', False), - related_name=kwargs.pop('related_name', None), - limit_choices_to=kwargs.pop('limit_choices_to', None), - lookup_overrides=kwargs.pop('lookup_overrides', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) - kwargs['primary_key'] = True - IntegerField.__init__(self, **kwargs) - -class ManyToOneRel: - def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, - max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False): - try: - self.to = to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert to == RECURSIVE_RELATIONSHIP_CONSTANT, "'to' must be either a model or the string '%s'" % RECURSIVE_RELATIONSHIP_CONSTANT - self.to = to - self.field_name = field_name - self.num_in_admin, self.edit_inline = num_in_admin, edit_inline - self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin - self.num_extra_on_change, self.related_name = num_extra_on_change, related_name - self.limit_choices_to = limit_choices_to or {} - self.lookup_overrides = lookup_overrides or {} - self.raw_id_admin = raw_id_admin - - def get_related_field(self): - "Returns the Field in the 'to' object to which this relationship is tied." - return self.to.get_field(self.field_name) - -class ManyToManyRel: - def __init__(self, to, singular=None, num_in_admin=0, related_name=None, - filter_interface=None, limit_choices_to=None, raw_id_admin=False): - self.to = to._meta - self.singular = singular or to._meta.object_name.lower() - self.num_in_admin = num_in_admin - self.related_name = related_name - self.filter_interface = filter_interface - self.limit_choices_to = limit_choices_to or {} - self.edit_inline = False - self.raw_id_admin = raw_id_admin - assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" - -class OneToOneRel(ManyToOneRel): - def __init__(self, to, field_name, num_in_admin=0, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, - raw_id_admin=False): - self.to, self.field_name = to._meta, field_name - self.num_in_admin, self.edit_inline = num_in_admin, edit_inline - self.related_name = related_name - self.limit_choices_to = limit_choices_to or {} - self.lookup_overrides = lookup_overrides or {} - self.raw_id_admin = raw_id_admin - -class BoundFieldLine(object): - def __init__(self, field_line, field_mapping, original, bound_field_class=BoundField): - self.bound_fields = [field.bind(field_mapping, original, bound_field_class) for field in field_line] + return [curry(forms.XMLLargeTextField, schema_path=self.schema_path)] - def __iter__(self): - for bound_field in self.bound_fields: - yield bound_field - - def __len__(self): - return len(self.bound_fields) - -class FieldLine(object): - def __init__(self, field_locator_func, linespec): - if isinstance(linespec, basestring): - self.fields = [field_locator_func(linespec)] - else: - self.fields = [field_locator_func(field_name) for field_name in linespec] - - def bind(self, field_mapping, original, bound_field_line_class=BoundFieldLine): - return bound_field_line_class(self, field_mapping, original) - - def __iter__(self): - for field in self.fields: - yield field - - def __len__(self): - return len(self.fields) - -class BoundFieldSet(object): - def __init__(self, field_set, field_mapping, original, bound_field_line_class=BoundFieldLine): - self.name = field_set.name - self.classes = field_set.classes - self.bound_field_lines = [field_line.bind(field_mapping,original, bound_field_line_class) for field_line in field_set] - - def __iter__(self): - for bound_field_line in self.bound_field_lines: - yield bound_field_line - - def __len__(self): - return len(self.bound_field_lines) - -class FieldSet(object): - def __init__(self, name, classes, field_locator_func, line_specs): - self.name = name - self.field_lines = [FieldLine(field_locator_func, line_spec) for line_spec in line_specs] - self.classes = classes - - def __repr__(self): - return "FieldSet:(%s,%s)" % (self.name, self.field_lines) - - def bind(self, field_mapping, original, bound_field_set_class=BoundFieldSet): - return bound_field_set_class(self, field_mapping, original) - - def __iter__(self): - for field_line in self.field_lines: - yield field_line - - def __len__(self): - return len(self.field_lines) +class OrderingField(IntegerField): + empty_strings_allowed=False + def __init__(self, with_respect_to, **kwargs): + self.wrt = with_respect_to + kwargs['null'] = True + IntegerField.__init__(self, **kwargs ) -class Admin: - def __init__(self, fields=None, js=None, list_display=None, list_filter=None, date_hierarchy=None, - save_as=False, ordering=None, search_fields=None, save_on_top=False, list_select_related=False): - self.fields = fields - self.js = js or [] - self.list_display = list_display or ['__repr__'] - self.list_filter = list_filter or [] - self.date_hierarchy = date_hierarchy - self.save_as, self.ordering = save_as, ordering - self.search_fields = search_fields or [] - self.save_on_top = save_on_top - self.list_select_related = list_select_related + def get_internal_type(self): + return "IntegerField" - def get_field_sets(self, opts): - if self.fields is None: - field_struct = ((None, { - 'fields': [f.name for f in opts.fields + opts.many_to_many if f.editable and not isinstance(f, AutoField)] - }),) - else: - field_struct = self.fields - new_fieldset_list = [] - for fieldset in field_struct: - name = fieldset[0] - fs_options = fieldset[1] - classes = fs_options.get('classes', ()) - line_specs = fs_options['fields'] - new_fieldset_list.append(FieldSet(name, classes, opts.get_field, line_specs)) - return new_fieldset_list + def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): + return [forms.HiddenField(name_prefix + self.name)] diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py new file mode 100644 index 0000000000..908aa75207 --- /dev/null +++ b/django/db/models/fields/related.py @@ -0,0 +1,718 @@ +from django.db import backend, connection, transaction +from django.db.models import signals +from django.db.models.fields import AutoField, Field, IntegerField, get_ul_class +from django.db.models.related import RelatedObject +from django.utils.translation import gettext_lazy, string_concat +from django.utils.functional import curry +from django.core import validators +from django import forms +from django.dispatch import dispatcher + +# For Python 2.3 +if not hasattr(__builtins__, 'set'): + from sets import Set as set + +# Values for Relation.edit_inline. +TABULAR, STACKED = 1, 2 + +RECURSIVE_RELATIONSHIP_CONSTANT = 'self' + +pending_lookups = {} + +def add_lookup(rel_cls, field): + name = field.rel.to + module = rel_cls.__module__ + key = (module, name) + pending_lookups.setdefault(key, []).append((rel_cls, field)) + +def do_pending_lookups(sender): + other_cls = sender + key = (other_cls.__module__, other_cls.__name__) + for rel_cls, field in pending_lookups.setdefault(key, []): + field.rel.to = other_cls + field.do_related_class(other_cls, rel_cls) + +dispatcher.connect(do_pending_lookups, signal=signals.class_prepared) + +def manipulator_valid_rel_key(f, self, field_data, all_data): + "Validates that the value is a valid foreign key" + klass = f.rel.to + try: + klass._default_manager.get(pk=field_data) + except klass.DoesNotExist: + raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name + +#HACK +class RelatedField(object): + def contribute_to_class(self, cls, name): + sup = super(RelatedField, self) + + # Add an accessor to allow easy determination of the related query path for this field + self.related_query_name = curry(self._get_related_query_name, cls._meta) + + if hasattr(sup, 'contribute_to_class'): + sup.contribute_to_class(cls, name) + other = self.rel.to + if isinstance(other, basestring): + if other == RECURSIVE_RELATIONSHIP_CONSTANT: + self.rel.to = cls.__name__ + add_lookup(cls, self) + else: + self.do_related_class(other, cls) + + def set_attributes_from_rel(self): + self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name) + self.verbose_name = self.verbose_name or self.rel.to._meta.verbose_name + self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name + + def do_related_class(self, other, cls): + self.set_attributes_from_rel() + related = RelatedObject(other, cls, self) + self.contribute_to_related_class(other, related) + + def _get_related_query_name(self, opts): + # This method defines the name that can be used to identify this related object + # in a table-spanning query. It uses the lower-cased object_name by default, + # but this can be overridden with the "related_name" option. + return self.rel.related_name or opts.object_name.lower() + +class SingleRelatedObjectDescriptor(object): + # This class provides the functionality that makes the related-object + # managers available as attributes on a model class, for fields that have + # a single "remote" value, on the class pointed to by a related field. + # In the example "place.restaurant", the restaurant attribute is a + # SingleRelatedObjectDescriptor instance. + def __init__(self, related): + self.related = related + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name + + params = {'%s__pk' % self.related.field.name: instance._get_pk_val()} + rel_obj = self.related.model._default_manager.get(**params) + return rel_obj + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name + # Set the value of the related field + setattr(value, self.related.field.rel.get_related_field().attname, instance) + + # Clear the cache, if it exists + try: + delattr(value, self.related.field.get_cache_name()) + except AttributeError: + pass + +class ReverseSingleRelatedObjectDescriptor(object): + # This class provides the functionality that makes the related-object + # managers available as attributes on a model class, for fields that have + # a single "remote" value, on the class that defines the related field. + # In the example "choice.poll", the poll attribute is a + # ReverseSingleRelatedObjectDescriptor instance. + def __init__(self, field_with_rel): + self.field = field_with_rel + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "%s must be accessed via instance" % self.field.name + cache_name = self.field.get_cache_name() + try: + return getattr(instance, cache_name) + except AttributeError: + val = getattr(instance, self.field.attname) + if val is None: + # If NULL is an allowed value, return it. + if self.field.null: + return None + raise self.field.rel.to.DoesNotExist + other_field = self.field.rel.get_related_field() + if other_field.rel: + params = {'%s__pk' % self.field.rel.field_name: val} + else: + params = {'%s__exact' % self.field.rel.field_name: val} + rel_obj = self.field.rel.to._default_manager.get(**params) + setattr(instance, cache_name, rel_obj) + return rel_obj + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "%s must be accessed via instance" % self._field.name + # Set the value of the related field + try: + val = getattr(value, self.field.rel.get_related_field().attname) + except AttributeError: + val = None + setattr(instance, self.field.attname, val) + + # Clear the cache, if it exists + try: + delattr(instance, self.field.get_cache_name()) + except AttributeError: + pass + +class ForeignRelatedObjectsDescriptor(object): + # This class provides the functionality that makes the related-object + # managers available as attributes on a model class, for fields that have + # multiple "remote" values and have a ForeignKey pointed at them by + # some other model. In the example "poll.choice_set", the choice_set + # attribute is a ForeignRelatedObjectsDescriptor instance. + def __init__(self, related): + self.related = related # RelatedObject instance + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + rel_field = self.related.field + rel_model = self.related.model + + # Dynamically create a class that subclasses the related + # model's default manager. + superclass = self.related.model._default_manager.__class__ + + class RelatedManager(superclass): + def get_query_set(self): + return superclass.get_query_set(self).filter(**(self.core_filters)) + + def add(self, *objs): + for obj in objs: + setattr(obj, rel_field.name, instance) + obj.save() + add.alters_data = True + + def create(self, **kwargs): + new_obj = self.model(**kwargs) + self.add(new_obj) + return new_obj + create.alters_data = True + + # remove() and clear() are only provided if the ForeignKey can have a value of null. + if rel_field.null: + def remove(self, *objs): + val = getattr(instance, rel_field.rel.get_related_field().attname) + for obj in objs: + # Is obj actually part of this descriptor set? + if getattr(obj, rel_field.attname) == val: + setattr(obj, rel_field.name, None) + obj.save() + else: + raise rel_field.rel.to.DoesNotExist, "'%s' is not related to '%s'." % (obj, instance) + remove.alters_data = True + + def clear(self): + for obj in self.all(): + setattr(obj, rel_field.name, None) + obj.save() + clear.alters_data = True + + manager = RelatedManager() + manager.core_filters = {'%s__pk' % rel_field.name: getattr(instance, rel_field.rel.get_related_field().attname)} + manager.model = self.related.model + + return manager + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + manager = self.__get__(instance) + # If the foreign key can support nulls, then completely clear the related set. + # Otherwise, just move the named objects into the set. + if self.related.field.null: + manager.clear() + for obj in value: + manager.add(obj) + +def create_many_related_manager(superclass): + """Creates a manager that subclasses 'superclass' (which is a Manager) + and adds behavior for many-to-many related objects.""" + class ManyRelatedManager(superclass): + def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, + join_table=None, source_col_name=None, target_col_name=None): + super(ManyRelatedManager, self).__init__() + self.core_filters = core_filters + self.model = model + self.symmetrical = symmetrical + self.instance = instance + self.join_table = join_table + self.source_col_name = source_col_name + self.target_col_name = target_col_name + if instance: + self._pk_val = self.instance._get_pk_val() + + def get_query_set(self): + return superclass.get_query_set(self).filter(**(self.core_filters)) + + def add(self, *objs): + self._add_items(self.source_col_name, self.target_col_name, *objs) + + # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table + if self.symmetrical: + self._add_items(self.target_col_name, self.source_col_name, *objs) + add.alters_data = True + + def remove(self, *objs): + self._remove_items(self.source_col_name, self.target_col_name, *objs) + + # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table + if self.symmetrical: + self._remove_items(self.target_col_name, self.source_col_name, *objs) + remove.alters_data = True + + def clear(self): + self._clear_items(self.source_col_name) + + # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table + if self.symmetrical: + self._clear_items(self.target_col_name) + clear.alters_data = True + + def create(self, **kwargs): + new_obj = self.model(**kwargs) + new_obj.save() + self.add(new_obj) + return new_obj + create.alters_data = True + + def _add_items(self, source_col_name, target_col_name, *objs): + # join_table: name of the m2m link table + # source_col_name: the PK colname in join_table for the source object + # target_col_name: the PK colname in join_table for the target object + # *objs - objects to add + from django.db import connection + + # Add the newly created or already existing objects to the join table. + # First find out which items are already added, to avoid adding them twice + new_ids = set([obj._get_pk_val() for obj in objs]) + cursor = connection.cursor() + cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ + (target_col_name, self.join_table, source_col_name, + target_col_name, ",".join(['%s'] * len(new_ids))), + [self._pk_val] + list(new_ids)) + if cursor.rowcount is not None and cursor.rowcount != 0: + existing_ids = set([row[0] for row in cursor.fetchmany(cursor.rowcount)]) + else: + existing_ids = set() + + # Add the ones that aren't there already + for obj_id in (new_ids - existing_ids): + cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ + (self.join_table, source_col_name, target_col_name), + [self._pk_val, obj_id]) + transaction.commit_unless_managed() + + def _remove_items(self, source_col_name, target_col_name, *objs): + # source_col_name: the PK colname in join_table for the source object + # target_col_name: the PK colname in join_table for the target object + # *objs - objects to remove + from django.db import connection + + for obj in objs: + if not isinstance(obj, self.model): + raise ValueError, "objects to remove() must be %s instances" % self.model._meta.object_name + # Remove the specified objects from the join table + cursor = connection.cursor() + for obj in objs: + cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s = %%s" % \ + (self.join_table, source_col_name, target_col_name), + [self._pk_val, obj._get_pk_val()]) + transaction.commit_unless_managed() + + def _clear_items(self, source_col_name): + # source_col_name: the PK colname in join_table for the source object + from django.db import connection + cursor = connection.cursor() + cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ + (self.join_table, source_col_name), + [self._pk_val]) + transaction.commit_unless_managed() + + return ManyRelatedManager + +class ManyRelatedObjectsDescriptor(object): + # This class provides the functionality that makes the related-object + # managers available as attributes on a model class, for fields that have + # multiple "remote" values and have a ManyToManyField pointed at them by + # some other model (rather than having a ManyToManyField themselves). + # In the example "publication.article_set", the article_set attribute is a + # ManyRelatedObjectsDescriptor instance. + def __init__(self, related): + self.related = related # RelatedObject instance + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + # Dynamically create a class that subclasses the related + # model's default manager. + rel_model = self.related.model + superclass = rel_model._default_manager.__class__ + RelatedManager = create_many_related_manager(superclass) + + qn = backend.quote_name + manager = RelatedManager( + model=rel_model, + core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()}, + instance=instance, + symmetrical=False, + join_table=qn(self.related.field.m2m_db_table()), + source_col_name=qn(self.related.field.m2m_reverse_name()), + target_col_name=qn(self.related.field.m2m_column_name()) + ) + + return manager + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + manager = self.__get__(instance) + manager.clear() + for obj in value: + manager.add(obj) + +class ReverseManyRelatedObjectsDescriptor(object): + # This class provides the functionality that makes the related-object + # managers available as attributes on a model class, for fields that have + # multiple "remote" values and have a ManyToManyField defined in their + # model (rather than having another model pointed *at* them). + # In the example "article.publications", the publications attribute is a + # ReverseManyRelatedObjectsDescriptor instance. + def __init__(self, m2m_field): + self.field = m2m_field + + def __get__(self, instance, instance_type=None): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + # Dynamically create a class that subclasses the related + # model's default manager. + rel_model=self.field.rel.to + superclass = rel_model._default_manager.__class__ + RelatedManager = create_many_related_manager(superclass) + + qn = backend.quote_name + manager = RelatedManager( + model=rel_model, + core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()}, + instance=instance, + symmetrical=(self.field.rel.symmetrical and instance.__class__ == rel_model), + join_table=qn(self.field.m2m_db_table()), + source_col_name=qn(self.field.m2m_column_name()), + target_col_name=qn(self.field.m2m_reverse_name()) + ) + + return manager + + def __set__(self, instance, value): + if instance is None: + raise AttributeError, "Manager must be accessed via instance" + + manager = self.__get__(instance) + manager.clear() + for obj in value: + manager.add(obj) + +class ForeignKey(RelatedField, Field): + empty_strings_allowed = False + def __init__(self, to, to_field=None, **kwargs): + try: + to_name = to._meta.object_name.lower() + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + assert isinstance(to, basestring), "ForeignKey(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (to, RECURSIVE_RELATIONSHIP_CONSTANT) + else: + to_field = to_field or to._meta.pk.name + kwargs['verbose_name'] = kwargs.get('verbose_name', '') + + if kwargs.has_key('edit_inline_type'): + import warnings + warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") + kwargs['edit_inline'] = kwargs.pop('edit_inline_type') + + kwargs['rel'] = ManyToOneRel(to, to_field, + num_in_admin=kwargs.pop('num_in_admin', 3), + min_num_in_admin=kwargs.pop('min_num_in_admin', None), + max_num_in_admin=kwargs.pop('max_num_in_admin', None), + num_extra_on_change=kwargs.pop('num_extra_on_change', 1), + edit_inline=kwargs.pop('edit_inline', False), + related_name=kwargs.pop('related_name', None), + limit_choices_to=kwargs.pop('limit_choices_to', None), + lookup_overrides=kwargs.pop('lookup_overrides', None), + raw_id_admin=kwargs.pop('raw_id_admin', False)) + Field.__init__(self, **kwargs) + + self.db_index = True + + def get_attname(self): + return '%s_id' % self.name + + def get_validator_unique_lookup_type(self): + return '%s__%s__exact' % (self.name, self.rel.get_related_field().name) + + def prepare_field_objs_and_params(self, manipulator, name_prefix): + params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname} + if self.rel.raw_id_admin: + field_objs = self.get_manipulator_field_objs() + params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) + else: + if self.radio_admin: + field_objs = [forms.RadioSelectField] + params['ul_class'] = get_ul_class(self.radio_admin) + else: + if self.null: + field_objs = [forms.NullSelectField] + else: + field_objs = [forms.SelectField] + params['choices'] = self.get_choices_default() + return field_objs, params + + def get_manipulator_field_objs(self): + rel_field = self.rel.get_related_field() + if self.rel.raw_id_admin and not isinstance(rel_field, AutoField): + return rel_field.get_manipulator_field_objs() + else: + return [forms.IntegerField] + + def get_db_prep_save(self, value): + if value == '' or value == None: + return None + else: + return self.rel.get_related_field().get_db_prep_save(value) + + def flatten_data(self, follow, obj=None): + if not obj: + # In required many-to-one fields with only one available choice, + # select that one available choice. Note: For SelectFields + # (radio_admin=False), we have to check that the length of choices + # is *2*, not 1, because SelectFields always have an initial + # "blank" value. Otherwise (radio_admin=True), we check that the + # length is 1. + if not self.blank and (not self.rel.raw_id_admin or self.choices): + choice_list = self.get_choices_default() + if self.radio_admin and len(choice_list) == 1: + return {self.attname: choice_list[0][0]} + if not self.radio_admin and len(choice_list) == 2: + return {self.attname: choice_list[1][0]} + return Field.flatten_data(self, follow, obj) + + def contribute_to_class(self, cls, name): + super(ForeignKey, self).contribute_to_class(cls, name) + setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self)) + + def contribute_to_related_class(self, cls, related): + setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) + +class OneToOneField(RelatedField, IntegerField): + def __init__(self, to, to_field=None, **kwargs): + kwargs['verbose_name'] = kwargs.get('verbose_name', '') + to_field = to_field or to._meta.pk.name + + if kwargs.has_key('edit_inline_type'): + import warnings + warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") + kwargs['edit_inline'] = kwargs.pop('edit_inline_type') + + kwargs['rel'] = OneToOneRel(to, to_field, + num_in_admin=kwargs.pop('num_in_admin', 0), + edit_inline=kwargs.pop('edit_inline', False), + related_name=kwargs.pop('related_name', None), + limit_choices_to=kwargs.pop('limit_choices_to', None), + lookup_overrides=kwargs.pop('lookup_overrides', None), + raw_id_admin=kwargs.pop('raw_id_admin', False)) + kwargs['primary_key'] = True + IntegerField.__init__(self, **kwargs) + + self.db_index = True + + def get_attname(self): + return '%s_id' % self.name + + def get_validator_unique_lookup_type(self): + return '%s__%s__exact' % (self.name, self.rel.get_related_field().name) + + # TODO: Copied from ForeignKey... putting this in RelatedField adversely affects + # ManyToManyField. This works for now. + def prepare_field_objs_and_params(self, manipulator, name_prefix): + params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname} + if self.rel.raw_id_admin: + field_objs = self.get_manipulator_field_objs() + params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) + else: + if self.radio_admin: + field_objs = [forms.RadioSelectField] + params['ul_class'] = get_ul_class(self.radio_admin) + else: + if self.null: + field_objs = [forms.NullSelectField] + else: + field_objs = [forms.SelectField] + params['choices'] = self.get_choices_default() + return field_objs, params + + def contribute_to_class(self, cls, name): + super(OneToOneField, self).contribute_to_class(cls, name) + setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self)) + + def contribute_to_related_class(self, cls, related): + setattr(cls, related.get_accessor_name(), SingleRelatedObjectDescriptor(related)) + if not cls._meta.one_to_one_field: + cls._meta.one_to_one_field = self + +class ManyToManyField(RelatedField, Field): + def __init__(self, to, **kwargs): + kwargs['verbose_name'] = kwargs.get('verbose_name', None) + kwargs['rel'] = ManyToManyRel(to, kwargs.pop('singular', None), + num_in_admin=kwargs.pop('num_in_admin', 0), + related_name=kwargs.pop('related_name', None), + filter_interface=kwargs.pop('filter_interface', None), + limit_choices_to=kwargs.pop('limit_choices_to', None), + raw_id_admin=kwargs.pop('raw_id_admin', False), + symmetrical=kwargs.pop('symmetrical', True)) + if kwargs["rel"].raw_id_admin: + kwargs.setdefault("validator_list", []).append(self.isValidIDList) + Field.__init__(self, **kwargs) + + if self.rel.raw_id_admin: + msg = gettext_lazy('Separate multiple IDs with commas.') + else: + msg = gettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') + self.help_text = string_concat(self.help_text, msg) + + def get_manipulator_field_objs(self): + if self.rel.raw_id_admin: + return [forms.RawIdAdminField] + else: + choices = self.get_choices_default() + return [curry(forms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + + def get_choices_default(self): + return Field.get_choices(self, include_blank=False) + + def _get_m2m_db_table(self, opts): + "Function that can be curried to provide the m2m table name for this relation" + return '%s_%s' % (opts.db_table, self.name) + + def _get_m2m_column_name(self, related): + "Function that can be curried to provide the source column name for the m2m table" + # If this is an m2m relation to self, avoid the inevitable name clash + if related.model == related.parent_model: + return 'from_' + related.model._meta.object_name.lower() + '_id' + else: + return related.model._meta.object_name.lower() + '_id' + + def _get_m2m_reverse_name(self, related): + "Function that can be curried to provide the related column name for the m2m table" + # If this is an m2m relation to self, avoid the inevitable name clash + if related.model == related.parent_model: + return 'to_' + related.parent_model._meta.object_name.lower() + '_id' + else: + return related.parent_model._meta.object_name.lower() + '_id' + + def isValidIDList(self, field_data, all_data): + "Validates that the value is a valid list of foreign keys" + mod = self.rel.to + try: + pks = map(int, field_data.split(',')) + except ValueError: + # the CommaSeparatedIntegerField validator will catch this error + return + objects = mod._default_manager.in_bulk(pks) + if len(objects) != len(pks): + badkeys = [k for k in pks if k not in objects] + raise validators.ValidationError, ngettext("Please enter valid %(self)s IDs. The value %(value)r is invalid.", + "Please enter valid %(self)s IDs. The values %(value)r are invalid.", len(badkeys)) % { + 'self': self.verbose_name, + 'value': len(badkeys) == 1 and badkeys[0] or tuple(badkeys), + } + + def flatten_data(self, follow, obj = None): + new_data = {} + if obj: + instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()] + if self.rel.raw_id_admin: + new_data[self.name] = ",".join([str(id) for id in instance_ids]) + else: + new_data[self.name] = instance_ids + else: + # In required many-to-many fields with only one available choice, + # select that one available choice. + if not self.blank and not self.rel.edit_inline and not self.rel.raw_id_admin: + choices_list = self.get_choices_default() + if len(choices_list) == 1: + new_data[self.name] = [choices_list[0][0]] + return new_data + + def contribute_to_class(self, cls, name): + super(ManyToManyField, self).contribute_to_class(cls, name) + # Add the descriptor for the m2m relation + setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) + + # Set up the accessor for the m2m table name for the relation + self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) + + def contribute_to_related_class(self, cls, related): + # m2m relations to self do not have a ManyRelatedObjectsDescriptor, + # as it would be redundant - unless the field is non-symmetrical. + if related.model != related.parent_model or not self.rel.symmetrical: + # Add the descriptor for the m2m relation + setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related)) + + self.rel.singular = self.rel.singular or self.rel.to._meta.object_name.lower() + + # Set up the accessors for the column names on the m2m table + self.m2m_column_name = curry(self._get_m2m_column_name, related) + self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related) + + def set_attributes_from_rel(self): + pass + +class ManyToOneRel: + def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, + max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, + related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False): + try: + to._meta + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT + self.to, self.field_name = to, field_name + self.num_in_admin, self.edit_inline = num_in_admin, edit_inline + self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin + self.num_extra_on_change, self.related_name = num_extra_on_change, related_name + self.limit_choices_to = limit_choices_to or {} + self.lookup_overrides = lookup_overrides or {} + self.raw_id_admin = raw_id_admin + self.multiple = True + + def get_related_field(self): + "Returns the Field in the 'to' object to which this relationship is tied." + return self.to._meta.get_field(self.field_name) + +class OneToOneRel(ManyToOneRel): + def __init__(self, to, field_name, num_in_admin=0, edit_inline=False, + related_name=None, limit_choices_to=None, lookup_overrides=None, + raw_id_admin=False): + self.to, self.field_name = to, field_name + self.num_in_admin, self.edit_inline = num_in_admin, edit_inline + self.related_name = related_name + self.limit_choices_to = limit_choices_to or {} + self.lookup_overrides = lookup_overrides or {} + self.raw_id_admin = raw_id_admin + self.multiple = False + +class ManyToManyRel: + def __init__(self, to, singular=None, num_in_admin=0, related_name=None, + filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): + self.to = to + self.singular = singular or None + self.num_in_admin = num_in_admin + self.related_name = related_name + self.filter_interface = filter_interface + self.limit_choices_to = limit_choices_to or {} + self.edit_inline = False + self.raw_id_admin = raw_id_admin + self.symmetrical = symmetrical + self.multiple = True + + assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" diff --git a/django/db/models/loading.py b/django/db/models/loading.py new file mode 100644 index 0000000000..a9e0348f8e --- /dev/null +++ b/django/db/models/loading.py @@ -0,0 +1,71 @@ +"Utilities for loading models and the modules that contain them." + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models') + +_app_list = None # Cache of installed apps. +_app_models = {} # Dictionary of models against app label + # Each value is a dictionary of model name: model class + +def get_apps(): + "Returns a list of all installed modules that contain models." + global _app_list + if _app_list is not None: + return _app_list + _app_list = [] + for app_name in settings.INSTALLED_APPS: + try: + _app_list.append(__import__(app_name, '', '', ['models']).models) + except (ImportError, AttributeError), e: + pass + return _app_list + +def get_app(app_label): + "Returns the module containing the models for the given app_label." + for app_name in settings.INSTALLED_APPS: + if app_label == app_name.split('.')[-1]: + return __import__(app_name, '', '', ['models']).models + raise ImproperlyConfigured, "App with label %s could not be found" % app_label + +def get_models(app_mod=None): + """ + Given a module containing models, returns a list of the models. Otherwise + returns a list of all installed models. + """ + app_list = get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish. + if app_mod: + return _app_models.get(app_mod.__name__.split('.')[-2], {}).values() + else: + model_list = [] + for app_mod in app_list: + model_list.extend(get_models(app_mod)) + return model_list + +def get_model(app_label, model_name): + """ + Returns the model matching the given app_label and case-insensitive model_name. + Returns None if no model is found. + """ + get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish. + try: + model_dict = _app_models[app_label] + except KeyError: + return None + + try: + return model_dict[model_name.lower()] + except KeyError: + return None + +def register_models(app_label, *models): + """ + Register a set of models as belonging to an app. + """ + for model in models: + # Store as 'name: model' pair in a dictionary + # in the _app_models dictionary + model_name = model._meta.object_name.lower() + model_dict = _app_models.setdefault(app_label, {}) + model_dict[model_name] = model diff --git a/django/db/models/manager.py b/django/db/models/manager.py new file mode 100644 index 0000000000..d847631c82 --- /dev/null +++ b/django/db/models/manager.py @@ -0,0 +1,101 @@ +from django.utils.functional import curry +from django.db import backend, connection +from django.db.models.query import QuerySet +from django.dispatch import dispatcher +from django.db.models import signals +from django.utils.datastructures import SortedDict + +# Size of each "chunk" for get_iterator calls. +# Larger values are slightly faster at the expense of more storage space. +GET_ITERATOR_CHUNK_SIZE = 100 + +def ensure_default_manager(sender): + cls = sender + if not hasattr(cls, '_default_manager'): + # Create the default manager, if needed. + if hasattr(cls, 'objects'): + raise ValueError, "Model %s must specify a custom Manager, because it has a field named 'objects'" % name + cls.add_to_class('objects', Manager()) + +dispatcher.connect(ensure_default_manager, signal=signals.class_prepared) + +class Manager(object): + # Tracks each time a Manager instance is created. Used to retain order. + creation_counter = 0 + + def __init__(self): + super(Manager, self).__init__() + # Increase the creation counter, and save our local copy. + self.creation_counter = Manager.creation_counter + Manager.creation_counter += 1 + self.model = None + + def contribute_to_class(self, model, name): + # TODO: Use weakref because of possible memory leak / circular reference. + self.model = model + setattr(model, name, ManagerDescriptor(self)) + if not hasattr(model, '_default_manager') or self.creation_counter < model._default_manager.creation_counter: + model._default_manager = self + + ####################### + # PROXIES TO QUERYSET # + ####################### + + def get_query_set(self): + """Returns a new QuerySet object. Subclasses can override this method + to easily customise the behaviour of the Manager. + """ + return QuerySet(self.model) + + def all(self): + return self.get_query_set() + + def count(self): + return self.get_query_set().count() + + def dates(self, *args, **kwargs): + return self.get_query_set().dates(*args, **kwargs) + + def distinct(self, *args, **kwargs): + return self.get_query_set().distinct(*args, **kwargs) + + def extra(self, *args, **kwargs): + return self.get_query_set().extra(*args, **kwargs) + + def get(self, *args, **kwargs): + return self.get_query_set().get(*args, **kwargs) + + def filter(self, *args, **kwargs): + return self.get_query_set().filter(*args, **kwargs) + + def exclude(self, *args, **kwargs): + return self.get_query_set().exclude(*args, **kwargs) + + def in_bulk(self, *args, **kwargs): + return self.get_query_set().in_bulk(*args, **kwargs) + + def iterator(self, *args, **kwargs): + return self.get_query_set().iterator(*args, **kwargs) + + def latest(self, *args, **kwargs): + return self.get_query_set().latest(*args, **kwargs) + + def order_by(self, *args, **kwargs): + return self.get_query_set().order_by(*args, **kwargs) + + def select_related(self, *args, **kwargs): + return self.get_query_set().select_related(*args, **kwargs) + + def values(self, *args, **kwargs): + return self.get_query_set().values(*args, **kwargs) + +class ManagerDescriptor(object): + # This class ensures managers aren't accessible via model instances. + # For example, Poll.objects works, but poll_obj.objects raises AttributeError. + def __init__(self, manager): + self.manager = manager + + def __get__(self, instance, type=None): + if instance != None: + raise AttributeError, "Manager isn't accessible via %s instances" % type.__name__ + return self.manager diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py new file mode 100644 index 0000000000..fc553bc90c --- /dev/null +++ b/django/db/models/manipulators.py @@ -0,0 +1,330 @@ +from django.core.exceptions import ObjectDoesNotExist +from django import forms +from django.core import validators +from django.db.models.fields import FileField, AutoField +from django.dispatch import dispatcher +from django.db.models import signals +from django.utils.functional import curry +from django.utils.datastructures import DotExpandedDict, MultiValueDict +from django.utils.text import capfirst +import types + +def add_manipulators(sender): + cls = sender + cls.add_to_class('AddManipulator', AutomaticAddManipulator) + cls.add_to_class('ChangeManipulator', AutomaticChangeManipulator) + +dispatcher.connect(add_manipulators, signal=signals.class_prepared) + +class ManipulatorDescriptor(object): + # This class provides the functionality that makes the default model + # manipulators (AddManipulator and ChangeManipulator) available via the + # model class. + def __init__(self, name, base): + self.man = None # Cache of the manipulator class. + self.name = name + self.base = base + + def __get__(self, instance, model=None): + if instance != None: + raise AttributeError, "Manipulator cannot be accessed via instance" + else: + if not self.man: + # Create a class that inherits from the "Manipulator" class + # given in the model class (if specified) and the automatic + # manipulator. + bases = [self.base] + if hasattr(model, 'Manipulator'): + bases = [model.Manipulator] + bases + self.man = types.ClassType(self.name, tuple(bases), {}) + self.man._prepare(model) + return self.man + +class AutomaticManipulator(forms.Manipulator): + def _prepare(cls, model): + cls.model = model + cls.manager = model._default_manager + cls.opts = model._meta + for field_name_list in cls.opts.unique_together: + setattr(cls, 'isUnique%s' % '_'.join(field_name_list), curry(manipulator_validator_unique_together, field_name_list, cls.opts)) + for f in cls.opts.fields: + if f.unique_for_date: + setattr(cls, 'isUnique%sFor%s' % (f.name, f.unique_for_date), curry(manipulator_validator_unique_for_date, f, cls.opts.get_field(f.unique_for_date), cls.opts, 'date')) + if f.unique_for_month: + setattr(cls, 'isUnique%sFor%s' % (f.name, f.unique_for_month), curry(manipulator_validator_unique_for_date, f, cls.opts.get_field(f.unique_for_month), cls.opts, 'month')) + if f.unique_for_year: + setattr(cls, 'isUnique%sFor%s' % (f.name, f.unique_for_year), curry(manipulator_validator_unique_for_date, f, cls.opts.get_field(f.unique_for_year), cls.opts, 'year')) + _prepare = classmethod(_prepare) + + def contribute_to_class(cls, other_cls, name): + setattr(other_cls, name, ManipulatorDescriptor(name, cls)) + contribute_to_class = classmethod(contribute_to_class) + + def __init__(self, follow=None): + self.follow = self.opts.get_follow(follow) + self.fields = [] + + for f in self.opts.fields + self.opts.many_to_many: + if self.follow.get(f.name, False): + self.fields.extend(f.get_manipulator_fields(self.opts, self, self.change)) + + # Add fields for related objects. + for f in self.opts.get_all_related_objects(): + if self.follow.get(f.name, False): + fol = self.follow[f.name] + self.fields.extend(f.get_manipulator_fields(self.opts, self, self.change, fol)) + + # Add field for ordering. + if self.change and self.opts.get_ordered_objects(): + self.fields.append(formfields.CommaSeparatedIntegerField(field_name="order_")) + + def save(self, new_data): + # TODO: big cleanup when core fields go -> use recursive manipulators. + params = {} + for f in self.opts.fields: + # Fields with auto_now_add should keep their original value in the change stage. + auto_now_add = self.change and getattr(f, 'auto_now_add', False) + if self.follow.get(f.name, None) and not auto_now_add: + param = f.get_manipulator_new_data(new_data) + else: + if self.change: + param = getattr(self.original_object, f.attname) + else: + param = f.get_default() + params[f.attname] = param + + if self.change: + params[self.opts.pk.attname] = self.obj_key + + # First, save the basic object itself. + new_object = self.model(**params) + new_object.save() + + # Now that the object's been saved, save any uploaded files. + for f in self.opts.fields: + if isinstance(f, FileField): + f.save_file(new_data, new_object, self.change and self.original_object or None, self.change, rel=False) + + # Calculate which primary fields have changed. + if self.change: + self.fields_added, self.fields_changed, self.fields_deleted = [], [], [] + for f in self.opts.fields: + if not f.primary_key and str(getattr(self.original_object, f.attname)) != str(getattr(new_object, f.attname)): + self.fields_changed.append(f.verbose_name) + + # Save many-to-many objects. Example: Set sites for a poll. + for f in self.opts.many_to_many: + if self.follow.get(f.name, None): + if not f.rel.edit_inline: + if f.rel.raw_id_admin: + new_vals = new_data.get(f.name, ()) + else: + new_vals = new_data.getlist(f.name) + # First, clear the existing values. + rel_manager = getattr(new_object, f.name) + rel_manager.clear() + # Then, set the new values. + for n in new_vals: + rel_manager.add(f.rel.to._default_manager.get(pk=n)) + # TODO: Add to 'fields_changed' + + expanded_data = DotExpandedDict(dict(new_data)) + # Save many-to-one objects. Example: Add the Choice objects for a Poll. + for related in self.opts.get_all_related_objects(): + # Create obj_list, which is a DotExpandedDict such as this: + # [('0', {'id': ['940'], 'choice': ['This is the first choice']}), + # ('1', {'id': ['941'], 'choice': ['This is the second choice']}), + # ('2', {'id': [''], 'choice': ['']})] + child_follow = self.follow.get(related.name, None) + + if child_follow: + obj_list = expanded_data[related.var_name].items() + if not obj_list: + continue + + obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0]))) + + # For each related item... + for _, rel_new_data in obj_list: + + params = {} + + # Keep track of which core=True fields were provided. + # If all core fields were given, the related object will be saved. + # If none of the core fields were given, the object will be deleted. + # If some, but not all, of the fields were given, the validator would + # have caught that. + all_cores_given, all_cores_blank = True, True + + # Get a reference to the old object. We'll use it to compare the + # old to the new, to see which fields have changed. + old_rel_obj = None + if self.change: + if rel_new_data[related.opts.pk.name][0]: + try: + old_rel_obj = getattr(self.original_object, related.get_accessor_name()).get(**{'%s__exact' % related.opts.pk.name: rel_new_data[related.opts.pk.attname][0]}) + except ObjectDoesNotExist: + pass + + for f in related.opts.fields: + if f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) in (None, ''): + all_cores_given = False + elif f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) not in (None, ''): + all_cores_blank = False + # If this field isn't editable, give it the same value it had + # previously, according to the given ID. If the ID wasn't + # given, use a default value. FileFields are also a special + # case, because they'll be dealt with later. + + if f == related.field: + param = getattr(new_object, related.field.rel.field_name) + elif (not self.change) and isinstance(f, AutoField): + param = None + elif self.change and (isinstance(f, FileField) or not child_follow.get(f.name, None)): + if old_rel_obj: + param = getattr(old_rel_obj, f.column) + else: + param = f.get_default() + else: + param = f.get_manipulator_new_data(rel_new_data, rel=True) + if param != None: + params[f.attname] = param + + # Create the related item. + new_rel_obj = related.model(**params) + + # If all the core fields were provided (non-empty), save the item. + if all_cores_given: + new_rel_obj.save() + + # Save any uploaded files. + for f in related.opts.fields: + if child_follow.get(f.name, None): + if isinstance(f, FileField) and rel_new_data.get(f.name, False): + f.save_file(rel_new_data, new_rel_obj, self.change and old_rel_obj or None, old_rel_obj is not None, rel=True) + + # Calculate whether any fields have changed. + if self.change: + if not old_rel_obj: # This object didn't exist before. + self.fields_added.append('%s "%s"' % (related.opts.verbose_name, new_rel_obj)) + else: + for f in related.opts.fields: + if not f.primary_key and f != related.field and str(getattr(old_rel_obj, f.attname)) != str(getattr(new_rel_obj, f.attname)): + self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj)) + + # Save many-to-many objects. + for f in related.opts.many_to_many: + if child_follow.get(f.name, None) and not f.rel.edit_inline: + was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.attname]) + if self.change and was_changed: + self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj)) + + # If, in the change stage, all of the core fields were blank and + # the primary key (ID) was provided, delete the item. + if self.change and all_cores_blank and old_rel_obj: + new_rel_obj.delete() + self.fields_deleted.append('%s "%s"' % (related.opts.verbose_name, old_rel_obj)) + + # Save the order, if applicable. + if self.change and self.opts.get_ordered_objects(): + order = new_data['order_'] and map(int, new_data['order_'].split(',')) or [] + for rel_opts in self.opts.get_ordered_objects(): + getattr(new_object, 'set_%s_order' % rel_opts.object_name.lower())(order) + return new_object + + def get_related_objects(self): + return self.opts.get_followed_related_objects(self.follow) + + def flatten_data(self): + new_data = {} + obj = self.change and self.original_object or None + for f in self.opts.get_data_holders(self.follow): + fol = self.follow.get(f.name) + new_data.update(f.flatten_data(fol, obj)) + return new_data + +class AutomaticAddManipulator(AutomaticManipulator): + change = False + +class AutomaticChangeManipulator(AutomaticManipulator): + change = True + def __init__(self, obj_key, follow=None): + self.obj_key = obj_key + try: + self.original_object = self.manager.get(pk=obj_key) + except ObjectDoesNotExist: + # If the object doesn't exist, this might be a manipulator for a + # one-to-one related object that hasn't created its subobject yet. + # For example, this might be a Restaurant for a Place that doesn't + # yet have restaurant information. + if self.opts.one_to_one_field: + # Sanity check -- Make sure the "parent" object exists. + # For example, make sure the Place exists for the Restaurant. + # Let the ObjectDoesNotExist exception propagate up. + lookup_kwargs = self.opts.one_to_one_field.rel.limit_choices_to + lookup_kwargs['%s__exact' % self.opts.one_to_one_field.rel.field_name] = obj_key + self.opts.one_to_one_field.rel.to.get_model_module().get(**lookup_kwargs) + params = dict([(f.attname, f.get_default()) for f in self.opts.fields]) + params[self.opts.pk.attname] = obj_key + self.original_object = self.opts.get_model_module().Klass(**params) + else: + raise + super(AutomaticChangeManipulator, self).__init__(follow=follow) + +def manipulator_validator_unique_together(field_name_list, opts, self, field_data, all_data): + from django.db.models.fields.related import ManyToOneRel + from django.utils.text import get_text_list + field_list = [opts.get_field(field_name) for field_name in field_name_list] + if isinstance(field_list[0].rel, ManyToOneRel): + kwargs = {'%s__%s__iexact' % (field_name_list[0], field_list[0].rel.field_name): field_data} + else: + kwargs = {'%s__iexact' % field_name_list[0]: field_data} + for f in field_list[1:]: + # This is really not going to work for fields that have different + # form fields, e.g. DateTime. + # This validation needs to occur after html2python to be effective. + field_val = all_data.get(f.attname, None) + if field_val is None: + # This will be caught by another validator, assuming the field + # doesn't have blank=True. + return + if isinstance(f.rel, ManyToOneRel): + kwargs['%s__pk' % f.name] = field_val + else: + kwargs['%s__iexact' % f.name] = field_val + try: + old_obj = self.manager.get(**kwargs) + except ObjectDoesNotExist: + return + if hasattr(self, 'original_object') and self.original_object._get_pk_val() == old_obj._get_pk_val(): + pass + else: + raise validators.ValidationError, _("%(object)s with this %(type)s already exists for the given %(field)s.") % \ + {'object': capfirst(opts.verbose_name), 'type': field_list[0].verbose_name, 'field': get_text_list(field_name_list[1:], 'and')} + +def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_type, self, field_data, all_data): + from django.db.models.fields.related import ManyToOneRel + date_str = all_data.get(date_field.get_manipulator_field_names('')[0], None) + date_val = forms.DateField.html2python(date_str) + if date_val is None: + return # Date was invalid. This will be caught by another validator. + lookup_kwargs = {'%s__year' % date_field.name: date_val.year} + if isinstance(from_field.rel, ManyToOneRel): + lookup_kwargs['%s__pk' % from_field.name] = field_data + else: + lookup_kwargs['%s__iexact' % from_field.name] = field_data + if lookup_type in ('month', 'date'): + lookup_kwargs['%s__month' % date_field.name] = date_val.month + if lookup_type == 'date': + lookup_kwargs['%s__day' % date_field.name] = date_val.day + try: + old_obj = self.manager.get(**lookup_kwargs) + except ObjectDoesNotExist: + return + else: + if hasattr(self, 'original_object') and self.original_object._get_pk_val() == old_obj._get_pk_val(): + pass + else: + format_string = (lookup_type == 'date') and '%B %d, %Y' or '%B %Y' + raise validators.ValidationError, "Please enter a different %s. The one you entered is already being used for %s." % \ + (from_field.verbose_name, date_val.strftime(format_string)) diff --git a/django/db/models/options.py b/django/db/models/options.py new file mode 100644 index 0000000000..d1f5eeb756 --- /dev/null +++ b/django/db/models/options.py @@ -0,0 +1,269 @@ +from django.conf import settings +from django.db.models.related import RelatedObject +from django.db.models.fields.related import ManyToManyRel +from django.db.models.fields import AutoField, FieldDoesNotExist +from django.db.models.loading import get_models +from django.db.models.query import orderlist2sql +from django.db.models import Manager +from bisect import bisect +import re + +# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". +get_verbose_name = lambda class_name: re.sub('([A-Z])', ' \\1', class_name).lower().strip() + +DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', + 'unique_together', 'permissions', 'get_latest_by', + 'order_with_respect_to', 'app_label') + +class Options: + def __init__(self, meta): + self.fields, self.many_to_many = [], [] + self.module_name, self.verbose_name = None, None + self.verbose_name_plural = None + self.db_table = '' + self.ordering = [] + self.unique_together = [] + self.permissions = [] + self.object_name, self.app_label = None, None + self.get_latest_by = None + self.order_with_respect_to = None + self.admin = None + self.meta = meta + self.pk = None + self.has_auto_field = False + self.one_to_one_field = None + self.parents = [] + + def contribute_to_class(self, cls, name): + cls._meta = self + self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS + # First, construct the default values for these options. + self.object_name = cls.__name__ + self.module_name = self.object_name.lower() + self.verbose_name = get_verbose_name(self.object_name) + # Next, apply any overridden values from 'class Meta'. + if self.meta: + meta_attrs = self.meta.__dict__ + del meta_attrs['__module__'] + del meta_attrs['__doc__'] + for attr_name in DEFAULT_NAMES: + setattr(self, attr_name, meta_attrs.pop(attr_name, getattr(self, attr_name))) + # verbose_name_plural is a special case because it uses a 's' + # by default. + setattr(self, 'verbose_name_plural', meta_attrs.pop('verbose_name_plural', self.verbose_name + 's')) + # Any leftover attributes must be invalid. + if meta_attrs != {}: + raise TypeError, "'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()) + else: + self.verbose_name_plural = self.verbose_name + 's' + del self.meta + + def _prepare(self, model): + if self.order_with_respect_to: + self.order_with_respect_to = self.get_field(self.order_with_respect_to) + self.ordering = ('_order',) + else: + self.order_with_respect_to = None + + if self.pk is None: + auto = AutoField(verbose_name='ID', primary_key=True) + auto.creation_counter = -1 + model.add_to_class('id', auto) + + # If the db_table wasn't provided, use the app_label + module_name. + if not self.db_table: + self.db_table = "%s_%s" % (self.app_label, self.module_name) + + def add_field(self, field): + # Insert the given field in the order in which it was created, using + # the "creation_counter" attribute of the field. + # Move many-to-many related fields from self.fields into self.many_to_many. + if field.rel and isinstance(field.rel, ManyToManyRel): + self.many_to_many.insert(bisect(self.many_to_many, field), field) + else: + self.fields.insert(bisect(self.fields, field), field) + if not self.pk and field.primary_key: + self.pk = field + + def __repr__(self): + return '<Options for %s>' % self.object_name + + def get_field(self, name, many_to_many=True): + "Returns the requested field by name. Raises FieldDoesNotExist on error." + to_search = many_to_many and (self.fields + self.many_to_many) or self.fields + for f in to_search: + if f.name == name: + return f + raise FieldDoesNotExist, "name=%s" % name + + def get_order_sql(self, table_prefix=''): + "Returns the full 'ORDER BY' clause for this object, according to self.ordering." + if not self.ordering: return '' + pre = table_prefix and (table_prefix + '.') or '' + return 'ORDER BY ' + orderlist2sql(self.ordering, self, pre) + + def get_add_permission(self): + return 'add_%s' % self.object_name.lower() + + def get_change_permission(self): + return 'change_%s' % self.object_name.lower() + + def get_delete_permission(self): + return 'delete_%s' % self.object_name.lower() + + def get_all_related_objects(self): + try: # Try the cache first. + return self._all_related_objects + except AttributeError: + rel_objs = [] + for klass in get_models(): + for f in klass._meta.fields: + if f.rel and self == f.rel.to._meta: + rel_objs.append(RelatedObject(f.rel.to, klass, f)) + self._all_related_objects = rel_objs + return rel_objs + + def get_followed_related_objects(self, follow=None): + if follow == None: + follow = self.get_follow() + return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] + + def get_data_holders(self, follow=None): + if follow == None: + follow = self.get_follow() + return [f for f in self.fields + self.many_to_many + self.get_all_related_objects() if follow.get(f.name, None)] + + def get_follow(self, override=None): + follow = {} + for f in self.fields + self.many_to_many + self.get_all_related_objects(): + if override and override.has_key(f.name): + child_override = override[f.name] + else: + child_override = None + fol = f.get_follow(child_override) + if fol != None: + follow[f.name] = fol + return follow + + def get_all_related_many_to_many_objects(self): + try: # Try the cache first. + return self._all_related_many_to_many_objects + except AttributeError: + rel_objs = [] + for klass in get_models(): + for f in klass._meta.many_to_many: + if f.rel and self == f.rel.to._meta: + rel_objs.append(RelatedObject(f.rel.to, klass, f)) + self._all_related_many_to_many_objects = rel_objs + return rel_objs + + def get_ordered_objects(self): + "Returns a list of Options objects that are ordered with respect to this object." + if not hasattr(self, '_ordered_objects'): + objects = [] + # TODO + #for klass in get_models(get_app(self.app_label)): + # opts = klass._meta + # if opts.order_with_respect_to and opts.order_with_respect_to.rel \ + # and self == opts.order_with_respect_to.rel.to._meta: + # objects.append(opts) + self._ordered_objects = objects + return self._ordered_objects + + def has_field_type(self, field_type, follow=None): + """ + Returns True if this object's admin form has at least one of the given + field_type (e.g. FileField). + """ + # TODO: follow + if not hasattr(self, '_field_types'): + self._field_types = {} + if not self._field_types.has_key(field_type): + try: + # First check self.fields. + for f in self.fields: + if isinstance(f, field_type): + raise StopIteration + # Failing that, check related fields. + for related in self.get_followed_related_objects(follow): + for f in related.opts.fields: + if isinstance(f, field_type): + raise StopIteration + except StopIteration: + self._field_types[field_type] = True + else: + self._field_types[field_type] = False + return self._field_types[field_type] + +class AdminOptions: + def __init__(self, fields=None, js=None, list_display=None, list_filter=None, + date_hierarchy=None, save_as=False, ordering=None, search_fields=None, + save_on_top=False, list_select_related=False, manager=None, list_per_page=100): + self.fields = fields + self.js = js or [] + self.list_display = list_display or ['__str__'] + self.list_filter = list_filter or [] + self.date_hierarchy = date_hierarchy + self.save_as, self.ordering = save_as, ordering + self.search_fields = search_fields or [] + self.save_on_top = save_on_top + self.list_select_related = list_select_related + self.list_per_page = list_per_page + self.manager = manager or Manager() + + def get_field_sets(self, opts): + "Returns a list of AdminFieldSet objects for this AdminOptions object." + if self.fields is None: + field_struct = ((None, {'fields': [f.name for f in opts.fields + opts.many_to_many if f.editable and not isinstance(f, AutoField)]}),) + else: + field_struct = self.fields + new_fieldset_list = [] + for fieldset in field_struct: + fs_options = fieldset[1] + classes = fs_options.get('classes', ()) + description = fs_options.get('description', '') + new_fieldset_list.append(AdminFieldSet(fieldset[0], classes, + opts.get_field, fs_options['fields'], description)) + return new_fieldset_list + + def contribute_to_class(self, cls, name): + cls._meta.admin = self + # Make sure the admin manager has access to the model + self.manager.model = cls + +class AdminFieldSet(object): + def __init__(self, name, classes, field_locator_func, line_specs, description): + self.name = name + self.field_lines = [AdminFieldLine(field_locator_func, line_spec) for line_spec in line_specs] + self.classes = classes + self.description = description + + def __repr__(self): + return "FieldSet: (%s, %s)" % (self.name, self.field_lines) + + def bind(self, field_mapping, original, bound_field_set_class): + return bound_field_set_class(self, field_mapping, original) + + def __iter__(self): + for field_line in self.field_lines: + yield field_line + + def __len__(self): + return len(self.field_lines) + +class AdminFieldLine(object): + def __init__(self, field_locator_func, linespec): + if isinstance(linespec, basestring): + self.fields = [field_locator_func(linespec)] + else: + self.fields = [field_locator_func(field_name) for field_name in linespec] + + def bind(self, field_mapping, original, bound_field_line_class): + return bound_field_line_class(self, field_mapping, original) + + def __iter__(self): + for field in self.fields: + yield field + + def __len__(self): + return len(self.fields) diff --git a/django/db/models/query.py b/django/db/models/query.py new file mode 100644 index 0000000000..365ead2a3a --- /dev/null +++ b/django/db/models/query.py @@ -0,0 +1,888 @@ +from django.db import backend, connection, transaction +from django.db.models.fields import DateField, FieldDoesNotExist +from django.db.models import signals +from django.dispatch import dispatcher +from django.utils.datastructures import SortedDict + +import operator + +# For Python 2.3 +if not hasattr(__builtins__, 'set'): + from sets import Set as set + +LOOKUP_SEPARATOR = '__' + +# Size of each "chunk" for get_iterator calls. +# Larger values are slightly faster at the expense of more storage space. +GET_ITERATOR_CHUNK_SIZE = 100 + +#################### +# HELPER FUNCTIONS # +#################### + +# Django currently supports two forms of ordering. +# Form 1 (deprecated) example: +# order_by=(('pub_date', 'DESC'), ('headline', 'ASC'), (None, 'RANDOM')) +# Form 2 (new-style) example: +# order_by=('-pub_date', 'headline', '?') +# Form 1 is deprecated and will no longer be supported for Django's first +# official release. The following code converts from Form 1 to Form 2. + +LEGACY_ORDERING_MAPPING = {'ASC': '_', 'DESC': '-_', 'RANDOM': '?'} + +def handle_legacy_orderlist(order_list): + if not order_list or isinstance(order_list[0], basestring): + return order_list + else: + import warnings + new_order_list = [LEGACY_ORDERING_MAPPING[j.upper()].replace('_', str(i)) for i, j in order_list] + warnings.warn("%r ordering syntax is deprecated. Use %r instead." % (order_list, new_order_list), DeprecationWarning) + return new_order_list + +def orderfield2column(f, opts): + try: + return opts.get_field(f, False).column + except FieldDoesNotExist: + return f + +def orderlist2sql(order_list, opts, prefix=''): + if prefix.endswith('.'): + prefix = backend.quote_name(prefix[:-1]) + '.' + output = [] + for f in handle_legacy_orderlist(order_list): + if f.startswith('-'): + output.append('%s%s DESC' % (prefix, backend.quote_name(orderfield2column(f[1:], opts)))) + elif f == '?': + output.append(backend.get_random_function_sql()) + else: + output.append('%s%s ASC' % (prefix, backend.quote_name(orderfield2column(f, opts)))) + return ', '.join(output) + +def quote_only_if_word(word): + if ' ' in word: + return word + else: + return backend.quote_name(word) + +class QuerySet(object): + "Represents a lazy database lookup for a set of objects" + def __init__(self, model=None): + self.model = model + self._filters = Q() + self._order_by = None # Ordering, e.g. ('date', '-name'). If None, use model's ordering. + self._select_related = False # Whether to fill cache for related objects. + self._distinct = False # Whether the query should use SELECT DISTINCT. + self._select = {} # Dictionary of attname -> SQL. + self._where = [] # List of extra WHERE clauses to use. + self._params = [] # List of params to use for extra WHERE clauses. + self._tables = [] # List of extra tables to use. + self._offset = None # OFFSET clause + self._limit = None # LIMIT clause + self._result_cache = None + + ######################## + # PYTHON MAGIC METHODS # + ######################## + + def __repr__(self): + return repr(self._get_data()) + + def __len__(self): + return len(self._get_data()) + + def __iter__(self): + return iter(self._get_data()) + + def __getitem__(self, k): + "Retrieve an item or slice from the set of results." + if self._result_cache is None: + if isinstance(k, slice): + # Offset: + if self._offset is None: + offset = k.start + elif k.start is None: + offset = self._offset + else: + offset = self._offset + k.start + # Now adjust offset to the bounds of any existing limit: + if self._limit is not None and k.start is not None: + limit = self._limit - k.start + else: + limit = self._limit + + # Limit: + if k.stop is not None and k.start is not None: + if limit is None: + limit = k.stop - k.start + else: + limit = min((k.stop - k.start), limit) + else: + if limit is None: + limit = k.stop + else: + if k.stop is not None: + limit = min(k.stop, limit) + + if k.step is None: + return self._clone(_offset=offset, _limit=limit) + else: + return list(self._clone(_offset=offset, _limit=limit))[::k.step] + else: + return self._clone(_offset=k, _limit=1).get() + else: + return self._result_cache[k] + + def __and__(self, other): + combined = self._combine(other) + combined._filters = self._filters & other._filters + return combined + + def __or__(self, other): + combined = self._combine(other) + combined._filters = self._filters | other._filters + return combined + + #################################### + # METHODS THAT DO DATABASE QUERIES # + #################################### + + def iterator(self): + "Performs the SELECT database lookup of this QuerySet." + # self._select is a dictionary, and dictionaries' key order is + # undefined, so we convert it to a list of tuples. + extra_select = self._select.items() + + cursor = connection.cursor() + select, sql, params = self._get_sql_clause() + cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) + fill_cache = self._select_related + index_end = len(self.model._meta.fields) + while 1: + rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) + if not rows: + raise StopIteration + for row in rows: + if fill_cache: + obj, index_end = get_cached_row(self.model, row, 0) + else: + obj = self.model(*row[:index_end]) + for i, k in enumerate(extra_select): + setattr(obj, k[0], row[index_end+i]) + yield obj + + def count(self): + "Performs a SELECT COUNT() and returns the number of records as an integer." + counter = self._clone() + counter._order_by = () + counter._offset = None + counter._limit = None + counter._select_related = False + select, sql, params = counter._get_sql_clause() + cursor = connection.cursor() + cursor.execute("SELECT COUNT(*)" + sql, params) + return cursor.fetchone()[0] + + def get(self, *args, **kwargs): + "Performs the SELECT and returns a single object matching the given keyword arguments." + clone = self.filter(*args, **kwargs) + if not clone._order_by: + clone._order_by = () + obj_list = list(clone) + if len(obj_list) < 1: + raise self.model.DoesNotExist, "%s does not exist for %s" % (self.model._meta.object_name, kwargs) + assert len(obj_list) == 1, "get() returned more than one %s -- it returned %s! Lookup parameters were %s" % (self.model._meta.object_name, len(obj_list), kwargs) + return obj_list[0] + + def latest(self, field_name=None): + """ + Returns the latest object, according to the model's 'get_latest_by' + option or optional given field_name. + """ + latest_by = field_name or self.model._meta.get_latest_by + assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model" + assert self._limit is None and self._offset is None, \ + "Cannot change a query once a slice has been taken." + return self._clone(_limit=1, _order_by=('-'+latest_by,)).get() + + def in_bulk(self, id_list): + """ + Returns a dictionary mapping each of the given IDs to the object with + that ID. + """ + assert self._limit is None and self._offset is None, \ + "Cannot use 'limit' or 'offset' with in_bulk" + assert isinstance(id_list, (tuple, list)), "in_bulk() must be provided with a list of IDs." + id_list = list(id_list) + if id_list == []: + return {} + qs = self._clone() + qs._where.append("%s.%s IN (%s)" % (backend.quote_name(self.model._meta.db_table), backend.quote_name(self.model._meta.pk.column), ",".join(['%s'] * len(id_list)))) + qs._params.extend(id_list) + return dict([(obj._get_pk_val(), obj) for obj in qs.iterator()]) + + def delete(self): + """ + Deletes the records in the current QuerySet. + """ + assert self._limit is None and self._offset is None, \ + "Cannot use 'limit' or 'offset' with delete." + + del_query = self._clone() + + # disable non-supported fields + del_query._select_related = False + del_query._order_by = [] + + # Delete objects in chunks to prevent an the list of + # related objects from becoming too long + more_objects = True + while more_objects: + # Collect all the objects to be deleted in this chunk, and all the objects + # that are related to the objects that are to be deleted + seen_objs = SortedDict() + more_objects = False + for object in del_query[0:GET_ITERATOR_CHUNK_SIZE]: + more_objects = True + object._collect_sub_objects(seen_objs) + + # If one or more objects were found, delete them. + # Otherwise, stop looping. + if more_objects: + delete_objects(seen_objs) + + # Clear the result cache, in case this QuerySet gets reused. + self._result_cache = None + delete.alters_data = True + + ################################################## + # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # + ################################################## + + def values(self, *fields): + return self._clone(klass=ValuesQuerySet, _fields=fields) + + def dates(self, field_name, kind, order='ASC'): + """ + Returns a list of datetime objects representing all available dates + for the given field_name, scoped to 'kind'. + """ + assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'." + assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'." + # Let the FieldDoesNotExist exception propagate. + field = self.model._meta.get_field(field_name, many_to_many=False) + assert isinstance(field, DateField), "%r isn't a DateField." % field_name + return self._clone(klass=DateQuerySet, _field=field, _kind=kind, _order=order) + + ################################################################## + # PUBLIC METHODS THAT ALTER ATTRIBUTES AND RETURN A NEW QUERYSET # + ################################################################## + + def filter(self, *args, **kwargs): + "Returns a new QuerySet instance with the args ANDed to the existing set." + return self._filter_or_exclude(Q, *args, **kwargs) + + def exclude(self, *args, **kwargs): + "Returns a new QuerySet instance with NOT (args) ANDed to the existing set." + return self._filter_or_exclude(QNot, *args, **kwargs) + + def _filter_or_exclude(self, qtype, *args, **kwargs): + if len(args) > 0 or len(kwargs) > 0: + assert self._limit is None and self._offset is None, \ + "Cannot filter a query once a slice has been taken." + + clone = self._clone() + if len(kwargs) > 0: + clone._filters = clone._filters & qtype(**kwargs) + if len(args) > 0: + clone._filters = clone._filters & reduce(operator.and_, args) + return clone + + def select_related(self, true_or_false=True): + "Returns a new QuerySet instance with '_select_related' modified." + return self._clone(_select_related=true_or_false) + + def order_by(self, *field_names): + "Returns a new QuerySet instance with the ordering changed." + assert self._limit is None and self._offset is None, \ + "Cannot reorder a query once a slice has been taken." + return self._clone(_order_by=field_names) + + def distinct(self, true_or_false=True): + "Returns a new QuerySet instance with '_distinct' modified." + return self._clone(_distinct=true_or_false) + + def extra(self, select=None, where=None, params=None, tables=None): + assert self._limit is None and self._offset is None, \ + "Cannot change a query once a slice has been taken" + clone = self._clone() + if select: clone._select.update(select) + if where: clone._where.extend(where) + if params: clone._params.extend(params) + if tables: clone._tables.extend(tables) + return clone + + ################### + # PRIVATE METHODS # + ################### + + def _clone(self, klass=None, **kwargs): + if klass is None: + klass = self.__class__ + c = klass() + c.model = self.model + c._filters = self._filters + c._order_by = self._order_by + c._select_related = self._select_related + c._distinct = self._distinct + c._select = self._select.copy() + c._where = self._where[:] + c._params = self._params[:] + c._tables = self._tables[:] + c._offset = self._offset + c._limit = self._limit + c.__dict__.update(kwargs) + return c + + def _combine(self, other): + assert self._limit is None and self._offset is None \ + and other._limit is None and other._offset is None, \ + "Cannot combine queries once a slice has been taken." + assert self._distinct == other._distinct, \ + "Cannot combine a unique query with a non-unique query" + # use 'other's order by + # (so that A.filter(args1) & A.filter(args2) does the same as + # A.filter(args1).filter(args2) + combined = other._clone() + # If 'self' is ordered and 'other' isn't, propagate 'self's ordering + if (self._order_by is not None and len(self._order_by) > 0) and \ + (combined._order_by is None or len(combined._order_by) == 0): + combined._order_by = self._order_by + return combined + + def _get_data(self): + if self._result_cache is None: + self._result_cache = list(self.iterator()) + return self._result_cache + + def _get_sql_clause(self): + opts = self.model._meta + + # Construct the fundamental parts of the query: SELECT X FROM Y WHERE Z. + select = ["%s.%s" % (backend.quote_name(opts.db_table), backend.quote_name(f.column)) for f in opts.fields] + tables = [quote_only_if_word(t) for t in self._tables] + joins = SortedDict() + where = self._where[:] + params = self._params[:] + + # Convert self._filters into SQL. + tables2, joins2, where2, params2 = self._filters.get_sql(opts) + tables.extend(tables2) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + + # Add additional tables and WHERE clauses based on select_related. + if self._select_related: + fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table]) + + # Add any additional SELECTs. + if self._select: + select.extend(['(%s) AS %s' % (quote_only_if_word(s[1]), backend.quote_name(s[0])) for s in self._select.items()]) + + # Start composing the body of the SQL statement. + sql = [" FROM", backend.quote_name(opts.db_table)] + + # Compose the join dictionary into SQL describing the joins. + if joins: + sql.append(" ".join(["%s %s AS %s ON %s" % (join_type, table, alias, condition) + for (alias, (table, join_type, condition)) in joins.items()])) + + # Compose the tables clause into SQL. + if tables: + sql.append(", " + ", ".join(tables)) + + # Compose the where clause into SQL. + if where: + sql.append(where and "WHERE " + " AND ".join(where)) + + # ORDER BY clause + order_by = [] + if self._order_by is not None: + ordering_to_use = self._order_by + else: + ordering_to_use = opts.ordering + for f in handle_legacy_orderlist(ordering_to_use): + if f == '?': # Special case. + order_by.append(backend.get_random_function_sql()) + else: + if f.startswith('-'): + col_name = f[1:] + order = "DESC" + else: + col_name = f + order = "ASC" + if "." in col_name: + table_prefix, col_name = col_name.split('.', 1) + table_prefix = backend.quote_name(table_prefix) + '.' + else: + # Use the database table as a column prefix if it wasn't given, + # and if the requested column isn't a custom SELECT. + if "." not in col_name and col_name not in (self._select or ()): + table_prefix = backend.quote_name(opts.db_table) + '.' + else: + table_prefix = '' + order_by.append('%s%s %s' % (table_prefix, backend.quote_name(orderfield2column(col_name, opts)), order)) + if order_by: + sql.append("ORDER BY " + ", ".join(order_by)) + + # LIMIT and OFFSET clauses + if self._limit is not None: + sql.append("%s " % backend.get_limit_offset_sql(self._limit, self._offset)) + else: + assert self._offset is None, "'offset' is not allowed without 'limit'" + + return select, " ".join(sql), params + +class ValuesQuerySet(QuerySet): + def iterator(self): + # select_related and select aren't supported in values(). + self._select_related = False + self._select = {} + + # self._fields is a list of field names to fetch. + if self._fields: + columns = [self.model._meta.get_field(f, many_to_many=False).column for f in self._fields] + field_names = self._fields + else: # Default to all fields. + columns = [f.column for f in self.model._meta.fields] + field_names = [f.attname for f in self.model._meta.fields] + + cursor = connection.cursor() + select, sql, params = self._get_sql_clause() + select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] + cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) + while 1: + rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) + if not rows: + raise StopIteration + for row in rows: + yield dict(zip(field_names, row)) + + def _clone(self, klass=None, **kwargs): + c = super(ValuesQuerySet, self)._clone(klass, **kwargs) + c._fields = self._fields[:] + return c + +class DateQuerySet(QuerySet): + def iterator(self): + from django.db.backends.util import typecast_timestamp + self._order_by = () # Clear this because it'll mess things up otherwise. + if self._field.null: + date_query._where.append('%s.%s IS NOT NULL' % \ + (backend.quote_name(self.model._meta.db_table), backend.quote_name(self._field.column))) + select, sql, params = self._get_sql_clause() + sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ + (backend.get_date_trunc_sql(self._kind, '%s.%s' % (backend.quote_name(self.model._meta.db_table), + backend.quote_name(self._field.column))), sql, self._order) + cursor = connection.cursor() + cursor.execute(sql, params) + # We have to manually run typecast_timestamp(str()) on the results, because + # MySQL doesn't automatically cast the result of date functions as datetime + # objects -- MySQL returns the values as strings, instead. + return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()] + + def _clone(self, klass=None, **kwargs): + c = super(DateQuerySet, self)._clone(klass, **kwargs) + c._field = self._field + c._kind = self._kind + c._order = self._order + return c + +class QOperator: + "Base class for QAnd and QOr" + def __init__(self, *args): + self.args = args + + def get_sql(self, opts): + tables, joins, where, params = [], SortedDict(), [], [] + for val in self.args: + tables2, joins2, where2, params2 = val.get_sql(opts) + tables.extend(tables2) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + if where: + return tables, joins, ['(%s)' % self.operator.join(where)], params + return tables, joins, [], params + +class QAnd(QOperator): + "Encapsulates a combined query that uses 'AND'." + operator = ' AND ' + def __or__(self, other): + return QOr(self, other) + + def __and__(self, other): + if isinstance(other, QAnd): + return QAnd(*(self.args+other.args)) + elif isinstance(other, (Q, QOr)): + return QAnd(*(self.args+(other,))) + else: + raise TypeError, other + +class QOr(QOperator): + "Encapsulates a combined query that uses 'OR'." + operator = ' OR ' + def __and__(self, other): + return QAnd(self, other) + + def __or__(self, other): + if isinstance(other, QOr): + return QOr(*(self.args+other.args)) + elif isinstance(other, (Q, QAnd)): + return QOr(*(self.args+(other,))) + else: + raise TypeError, other + +class Q(object): + "Encapsulates queries as objects that can be combined logically." + def __init__(self, **kwargs): + self.kwargs = kwargs + + def __and__(self, other): + return QAnd(self, other) + + def __or__(self, other): + return QOr(self, other) + + def get_sql(self, opts): + return parse_lookup(self.kwargs.items(), opts) + +class QNot(Q): + "Encapsulates NOT (...) queries as objects" + + def get_sql(self, opts): + tables, joins, where, params = super(QNot, self).get_sql(opts) + where2 = ['(NOT (%s))' % " AND ".join(where)] + return tables, joins, where2, params + +def get_where_clause(lookup_type, table_prefix, field_name, value): + if table_prefix.endswith('.'): + table_prefix = backend.quote_name(table_prefix[:-1])+'.' + field_name = backend.quote_name(field_name) + try: + return '%s%s %s' % (table_prefix, field_name, (backend.OPERATOR_MAPPING[lookup_type] % '%s')) + except KeyError: + pass + if lookup_type == 'in': + return '%s%s IN (%s)' % (table_prefix, field_name, ','.join(['%s' for v in value])) + elif lookup_type == 'range': + return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name) + elif lookup_type in ('year', 'month', 'day'): + return "%s = %%s" % backend.get_date_extract_sql(lookup_type, table_prefix + field_name) + elif lookup_type == 'isnull': + return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or '')) + raise TypeError, "Got invalid lookup_type: %s" % repr(lookup_type) + +def get_cached_row(klass, row, index_start): + "Helper function that recursively returns an object with cache filled" + index_end = index_start + len(klass._meta.fields) + obj = klass(*row[index_start:index_end]) + for f in klass._meta.fields: + if f.rel and not f.null: + rel_obj, index_end = get_cached_row(f.rel.to, row, index_end) + setattr(obj, f.get_cache_name(), rel_obj) + return obj, index_end + +def fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen): + """ + Helper function that recursively populates the select, tables and where (in + place) for fill-cache queries. + """ + for f in opts.fields: + if f.rel and not f.null: + db_table = f.rel.to._meta.db_table + if db_table not in cache_tables_seen: + tables.append(backend.quote_name(db_table)) + else: # The table was already seen, so give it a table alias. + new_prefix = '%s%s' % (db_table, len(cache_tables_seen)) + tables.append('%s %s' % (backend.quote_name(db_table), backend.quote_name(new_prefix))) + db_table = new_prefix + cache_tables_seen.append(db_table) + where.append('%s.%s = %s.%s' % \ + (backend.quote_name(old_prefix), backend.quote_name(f.column), + backend.quote_name(db_table), backend.quote_name(f.rel.get_related_field().column))) + select.extend(['%s.%s' % (backend.quote_name(db_table), backend.quote_name(f2.column)) for f2 in f.rel.to._meta.fields]) + fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen) + +def parse_lookup(kwarg_items, opts): + # Helper function that handles converting API kwargs + # (e.g. "name__exact": "tom") to SQL. + + # 'joins' is a sorted dictionary describing the tables that must be joined + # to complete the query. The dictionary is sorted because creation order + # is significant; it is a dictionary to ensure uniqueness of alias names. + # + # Each key-value pair follows the form + # alias: (table, join_type, condition) + # where + # alias is the AS alias for the joined table + # table is the actual table name to be joined + # join_type is the type of join (INNER JOIN, LEFT OUTER JOIN, etc) + # condition is the where-like statement over which narrows the join. + # alias will be derived from the lookup list name. + # + # At present, this method only every returns INNER JOINs; the option is + # there for others to implement custom Q()s, etc that return other join + # types. + tables, joins, where, params = [], SortedDict(), [], [] + + for kwarg, value in kwarg_items: + if value is not None: + path = kwarg.split(LOOKUP_SEPARATOR) + # Extract the last elements of the kwarg. + # The very-last is the clause (equals, like, etc). + # The second-last is the table column on which the clause is + # to be performed. + # The exceptions to this are: + # 1) "pk", which is an implicit id__exact; + # if we find "pk", make the clause "exact', and insert + # a dummy name of None, which we will replace when + # we know which table column to grab as the primary key. + # 2) If there is only one part, assume it to be an __exact + clause = path.pop() + if clause == 'pk': + clause = 'exact' + path.append(None) + elif len(path) == 0: + path.append(clause) + clause = 'exact' + + if len(path) < 1: + raise TypeError, "Cannot parse keyword query %r" % kwarg + + tables2, joins2, where2, params2 = lookup_inner(path, clause, value, opts, opts.db_table, None) + tables.extend(tables2) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + return tables, joins, where, params + +class FieldFound(Exception): + "Exception used to short circuit field-finding operations." + pass + +def find_field(name, field_list, related_query): + """ + Finds a field with a specific name in a list of field instances. + Returns None if there are no matches, or several matches. + """ + if related_query: + matches = [f for f in field_list if f.field.related_query_name() == name] + else: + matches = [f for f in field_list if f.name == name] + if len(matches) != 1: + return None + return matches[0] + +def lookup_inner(path, clause, value, opts, table, column): + tables, joins, where, params = [], SortedDict(), [], [] + current_opts = opts + current_table = table + current_column = column + intermediate_table = None + join_required = False + + name = path.pop(0) + # Has the primary key been requested? If so, expand it out + # to be the name of the current class' primary key + if name is None: + name = current_opts.pk.name + + # Try to find the name in the fields associated with the current class + try: + # Does the name belong to a defined many-to-many field? + field = find_field(name, current_opts.many_to_many, False) + if field: + new_table = current_table + LOOKUP_SEPARATOR + name + new_opts = field.rel.to._meta + new_column = new_opts.pk.column + + # Need to create an intermediate table join over the m2m table + # This process hijacks current_table/column to point to the + # intermediate table. + current_table = "m2m_" + new_table + intermediate_table = field.m2m_db_table() + join_column = field.m2m_reverse_name() + intermediate_column = field.m2m_column_name() + + raise FieldFound + + # Does the name belong to a reverse defined many-to-many field? + field = find_field(name, current_opts.get_all_related_many_to_many_objects(), True) + if field: + new_table = current_table + LOOKUP_SEPARATOR + name + new_opts = field.opts + new_column = new_opts.pk.column + + # Need to create an intermediate table join over the m2m table. + # This process hijacks current_table/column to point to the + # intermediate table. + current_table = "m2m_" + new_table + intermediate_table = field.field.m2m_db_table() + join_column = field.field.m2m_column_name() + intermediate_column = field.field.m2m_reverse_name() + + raise FieldFound + + # Does the name belong to a one-to-many field? + field = find_field(name, current_opts.get_all_related_objects(), True) + if field: + new_table = table + LOOKUP_SEPARATOR + name + new_opts = field.opts + new_column = field.field.column + join_column = opts.pk.column + + # 1-N fields MUST be joined, regardless of any other conditions. + join_required = True + + raise FieldFound + + # Does the name belong to a one-to-one, many-to-one, or regular field? + field = find_field(name, current_opts.fields, False) + if field: + if field.rel: # One-to-One/Many-to-one field + new_table = current_table + LOOKUP_SEPARATOR + name + new_opts = field.rel.to._meta + new_column = new_opts.pk.column + join_column = field.column + + raise FieldFound + + except FieldFound: # Match found, loop has been shortcut. + pass + except: # Any other exception; rethrow + raise + else: # No match found. + raise TypeError, "Cannot resolve keyword '%s' into field" % name + + # Check to see if an intermediate join is required between current_table + # and new_table. + if intermediate_table: + joins[backend.quote_name(current_table)] = ( + backend.quote_name(intermediate_table), + "LEFT OUTER JOIN", + "%s.%s = %s.%s" % \ + (backend.quote_name(table), + backend.quote_name(current_opts.pk.column), + backend.quote_name(current_table), + backend.quote_name(intermediate_column)) + ) + + if path: + if len(path) == 1 and path[0] in (new_opts.pk.name, None) \ + and clause in ('exact', 'isnull') and not join_required: + # If the last name query is for a key, and the search is for + # isnull/exact, then the current (for N-1) or intermediate + # (for N-N) table can be used for the search - no need to join an + # extra table just to check the primary key. + new_table = current_table + else: + # There are 1 or more name queries pending, and we have ruled out + # any shortcuts; therefore, a join is required. + joins[backend.quote_name(new_table)] = ( + backend.quote_name(new_opts.db_table), + "INNER JOIN", + "%s.%s = %s.%s" % + (backend.quote_name(current_table), + backend.quote_name(join_column), + backend.quote_name(new_table), + backend.quote_name(new_column)) + ) + # If we have made the join, we don't need to tell subsequent + # recursive calls about the column name we joined on. + join_column = None + + # There are name queries remaining. Recurse deeper. + tables2, joins2, where2, params2 = lookup_inner(path, clause, value, new_opts, new_table, join_column) + + tables.extend(tables2) + joins.update(joins2) + where.extend(where2) + params.extend(params2) + else: + # Evaluate clause on current table. + if name in (current_opts.pk.name, None) and clause in ('exact', 'isnull') and current_column: + # If this is an exact/isnull key search, and the last pass + # found/introduced a current/intermediate table that we can use to + # optimize the query, then use that column name. + column = current_column + else: + column = field.column + + where.append(get_where_clause(clause, current_table + '.', column, value)) + params.extend(field.get_db_prep_lookup(clause, value)) + + return tables, joins, where, params + +def delete_objects(seen_objs): + "Iterate through a list of seen classes, and remove any instances that are referred to" + ordered_classes = seen_objs.keys() + ordered_classes.reverse() + + cursor = connection.cursor() + + for cls in ordered_classes: + seen_objs[cls] = seen_objs[cls].items() + seen_objs[cls].sort() + + # Pre notify all instances to be deleted + for pk_val, instance in seen_objs[cls]: + dispatcher.send(signal=signals.pre_delete, sender=cls, instance=instance) + + pk_list = [pk for pk,instance in seen_objs[cls]] + for related in cls._meta.get_all_related_many_to_many_objects(): + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ + (backend.quote_name(related.field.m2m_db_table()), + backend.quote_name(related.field.m2m_reverse_name()), + ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), + pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + for f in cls._meta.many_to_many: + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ + (backend.quote_name(f.m2m_db_table()), + backend.quote_name(f.m2m_column_name()), + ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), + pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + for field in cls._meta.fields: + if field.rel and field.null and field.rel.to in seen_objs: + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + cursor.execute("UPDATE %s SET %s=NULL WHERE %s IN (%s)" % \ + (backend.quote_name(cls._meta.db_table), + backend.quote_name(field.column), + backend.quote_name(cls._meta.pk.column), + ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), + pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + + # Now delete the actual data + for cls in ordered_classes: + seen_objs[cls].reverse() + pk_list = [pk for pk,instance in seen_objs[cls]] + for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE): + cursor.execute("DELETE FROM %s WHERE %s IN (%s)" % \ + (backend.quote_name(cls._meta.db_table), + backend.quote_name(cls._meta.pk.column), + ','.join(['%s' for pk in pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]])), + pk_list[offset:offset+GET_ITERATOR_CHUNK_SIZE]) + + # Last cleanup; set NULLs where there once was a reference to the object, + # NULL the primary key of the found objects, and perform post-notification. + for pk_val, instance in seen_objs[cls]: + for field in cls._meta.fields: + if field.rel and field.null and field.rel.to in seen_objs: + setattr(instance, field.attname, None) + + setattr(instance, cls._meta.pk.attname, None) + dispatcher.send(signal=signals.post_delete, sender=cls, instance=instance) + + transaction.commit_unless_managed() diff --git a/django/db/models/related.py b/django/db/models/related.py new file mode 100644 index 0000000000..4ab8cde5e7 --- /dev/null +++ b/django/db/models/related.py @@ -0,0 +1,132 @@ +class BoundRelatedObject(object): + def __init__(self, related_object, field_mapping, original): + self.relation = related_object + self.field_mappings = field_mapping[related_object.opts.module_name] + + def template_name(self): + raise NotImplementedError + + def __repr__(self): + return repr(self.__dict__) + +class RelatedObject(object): + def __init__(self, parent_model, model, field): + self.parent_model = parent_model + self.model = model + self.opts = model._meta + self.field = field + self.edit_inline = field.rel.edit_inline + self.name = self.opts.module_name + self.var_name = self.opts.object_name.lower() + + def flatten_data(self, follow, obj=None): + new_data = {} + rel_instances = self.get_list(obj) + for i, rel_instance in enumerate(rel_instances): + instance_data = {} + for f in self.opts.fields + self.opts.many_to_many: + # TODO: Fix for recursive manipulators. + fol = follow.get(f.name, None) + if fol: + field_data = f.flatten_data(fol, rel_instance) + for name, value in field_data.items(): + instance_data['%s.%d.%s' % (self.var_name, i, name)] = value + new_data.update(instance_data) + return new_data + + def extract_data(self, data): + """ + Pull out the data meant for inline objects of this class, + i.e. anything starting with our module name. + """ + return data # TODO + + def get_list(self, parent_instance=None): + "Get the list of this type of object from an instance of the parent class." + if parent_instance is not None: + attr = getattr(parent_instance, self.get_accessor_name()) + if self.field.rel.multiple: + # For many-to-many relationships, return a list of objects + # corresponding to the xxx_num_in_admin options of the field + objects = list(attr.all()) + + count = len(objects) + self.field.rel.num_extra_on_change + if self.field.rel.min_num_in_admin: + count = max(count, self.field.rel.min_num_in_admin) + if self.field.rel.max_num_in_admin: + count = min(count, self.field.rel.max_num_in_admin) + + change = count - len(objects) + if change > 0: + return objects + [None] * change + if change < 0: + return objects[:change] + else: # Just right + return objects + else: + # A one-to-one relationship, so just return the single related + # object + return [attr] + else: + return [None] * self.field.rel.num_in_admin + + def editable_fields(self): + "Get the fields in this class that should be edited inline." + return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] + + def get_follow(self, override=None): + if isinstance(override, bool): + if override: + over = {} + else: + return None + else: + if override: + over = override.copy() + elif self.edit_inline: + over = {} + else: + return None + + over[self.field.name] = False + return self.opts.get_follow(over) + + def get_manipulator_fields(self, opts, manipulator, change, follow): + if self.field.rel.multiple: + if change: + attr = getattr(manipulator.original_object, self.get_accessor_name()) + count = attr.count() + count += self.field.rel.num_extra_on_change + if self.field.rel.min_num_in_admin: + count = max(count, self.field.rel.min_num_in_admin) + if self.field.rel.max_num_in_admin: + count = min(count, self.field.rel.max_num_in_admin) + else: + count = self.field.rel.num_in_admin + else: + count = 1 + + fields = [] + for i in range(count): + for f in self.opts.fields + self.opts.many_to_many: + if follow.get(f.name, False): + prefix = '%s.%d.' % (self.var_name, i) + fields.extend(f.get_manipulator_fields(self.opts, manipulator, change, + name_prefix=prefix, rel=True)) + return fields + + def __repr__(self): + return "<RelatedObject: %s related to %s>" % (self.name, self.field.name) + + def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): + return bound_related_object_class(self, field_mapping, original) + + def get_accessor_name(self): + # This method encapsulates the logic that decides what name to give an + # accessor descriptor that retrieves related many-to-one or + # many-to-many objects. It uses the lower-cased object_name + "_set", + # but this can be overridden with the "related_name" option. + if self.field.rel.multiple: + return self.field.rel.related_name or (self.opts.object_name.lower() + '_set') + else: + return self.field.rel.related_name or (self.opts.object_name.lower()) diff --git a/django/db/models/signals.py b/django/db/models/signals.py new file mode 100644 index 0000000000..2171cb1bf3 --- /dev/null +++ b/django/db/models/signals.py @@ -0,0 +1,12 @@ +class_prepared = object() + +pre_init= object() +post_init = object() + +pre_save = object() +post_save = object() + +pre_delete = object() +post_delete = object() + +post_syncdb = object() diff --git a/django/db/transaction.py b/django/db/transaction.py new file mode 100644 index 0000000000..906995ca02 --- /dev/null +++ b/django/db/transaction.py @@ -0,0 +1,219 @@ +""" +This module implements a transaction manager that can be used to define +transaction handling in a request or view function. It is used by transaction +control middleware and decorators. + +The transaction manager can be in managed or in auto state. Auto state means the +system is using a commit-on-save strategy (actually it's more like +commit-on-change). As soon as the .save() or .delete() (or related) methods are +called, a commit is made. + +Managed transactions don't do those commits, but will need some kind of manual +or implicit commits or rollbacks. +""" + +import thread +from django.db import connection +from django.conf import settings + +class TransactionManagementError(Exception): + """ + This exception is thrown when something bad happens with transaction + management. + """ + pass + +# The state is a dictionary of lists. The key to the dict is the current +# thread and the list is handled as a stack of values. +state = {} + +# The dirty flag is set by *_unless_managed functions to denote that the +# code under transaction management has changed things to require a +# database commit. +dirty = {} + +def enter_transaction_management(): + """ + Enters transaction management for a running thread. It must be balanced with + the appropriate leave_transaction_management call, since the actual state is + managed as a stack. + + The state and dirty flag are carried over from the surrounding block or + from the settings, if there is no surrounding block (dirty is always false + when no current block is running). + """ + thread_ident = thread.get_ident() + if state.has_key(thread_ident) and state[thread_ident]: + state[thread_ident].append(state[thread_ident][-1]) + else: + state[thread_ident] = [] + state[thread_ident].append(settings.TRANSACTIONS_MANAGED) + if not dirty.has_key(thread_ident): + dirty[thread_ident] = False + +def leave_transaction_management(): + """ + Leaves transaction management for a running thread. A dirty flag is carried + over to the surrounding block, as a commit will commit all changes, even + those from outside. (Commits are on connection level.) + """ + thread_ident = thread.get_ident() + if state.has_key(thread_ident) and state[thread_ident]: + del state[thread_ident][-1] + else: + raise TransactionManagementError("This code isn't under transaction management") + if dirty.get(thread_ident, False): + rollback() + raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK") + dirty[thread_ident] = False + +def is_dirty(): + """ + Returns True if the current transaction requires a commit for changes to + happen. + """ + return dirty.get(thread.get_ident(), False) + +def set_dirty(): + """ + Sets a dirty flag for the current thread and code streak. This can be used + to decide in a managed block of code to decide whether there are open + changes waiting for commit. + """ + thread_ident = thread.get_ident() + if dirty.has_key(thread_ident): + dirty[thread_ident] = True + else: + raise TransactionManagementError("This code isn't under transaction management") + +def set_clean(): + """ + Resets a dirty flag for the current thread and code streak. This can be used + to decide in a managed block of code to decide whether a commit or rollback + should happen. + """ + thread_ident = thread.get_ident() + if dirty.has_key(thread_ident): + dirty[thread_ident] = False + else: + raise TransactionManagementError("This code isn't under transaction management") + +def is_managed(): + """ + Checks whether the transaction manager is in manual or in auto state. + """ + thread_ident = thread.get_ident() + if state.has_key(thread_ident): + if state[thread_ident]: + return state[thread_ident][-1] + return settings.TRANSACTIONS_MANAGED + +def managed(flag=True): + """ + Puts the transaction manager into a manual state: managed transactions have + to be committed explicitely by the user. If you switch off transaction + management and there is a pending commit/rollback, the data will be + commited. + """ + thread_ident = thread.get_ident() + top = state.get(thread_ident, None) + if top: + top[-1] = flag + if not flag and is_dirty(): + connection._commit() + set_clean() + else: + raise TransactionManagementError("This code isn't under transaction management") + +def commit_unless_managed(): + """ + Commits changes if the system is not in managed transaction mode. + """ + if not is_managed(): + connection._commit() + else: + set_dirty() + +def rollback_unless_managed(): + """ + Rolls back changes if the system is not in managed transaction mode. + """ + if not is_managed(): + connection._rollback() + else: + set_dirty() + +def commit(): + """ + Does the commit itself and resets the dirty flag. + """ + connection._commit() + set_clean() + +def rollback(): + """ + This function does the rollback itself and resets the dirty flag. + """ + connection._rollback() + set_clean() + +############## +# DECORATORS # +############## + +def autocommit(func): + """ + Decorator that activates commit on save. This is Django's default behavior; + this decorator is useful if you globally activated transaction management in + your settings file and want the default behavior in some view functions. + """ + def _autocommit(*args, **kw): + try: + enter_transaction_management() + managed(False) + return func(*args, **kw) + finally: + leave_transaction_management() + return _autocommit + +def commit_on_success(func): + """ + This decorator activates commit on response. This way, if the view function + runs successfully, a commit is made; if the viewfunc produces an exception, + a rollback is made. This is one of the most common ways to do transaction + control in web apps. + """ + def _commit_on_success(*args, **kw): + try: + enter_transaction_management() + managed(True) + try: + res = func(*args, **kw) + except Exception, e: + if is_dirty(): + rollback() + raise + else: + if is_dirty(): + commit() + return res + finally: + leave_transaction_management() + return _commit_on_success + +def commit_manually(func): + """ + Decorator that activates manual transaction control. It just disables + automatic transaction control and doesn't do any commit/rollback of its + own -- it's up to the user to call the commit and rollback functions + themselves. + """ + def _commit_manually(*args, **kw): + try: + enter_transaction_management() + managed(True) + return func(*args, **kw) + finally: + leave_transaction_management() + + return _commit_manually diff --git a/django/dispatch/__init__.py b/django/dispatch/__init__.py new file mode 100644 index 0000000000..bccae2a2da --- /dev/null +++ b/django/dispatch/__init__.py @@ -0,0 +1,6 @@ +"""Multi-consumer multi-producer dispatching mechanism +""" +__version__ = "1.0.0" +__author__ = "Patrick K. O'Brien" +__license__ = "BSD-style, see license.txt for details" + diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py new file mode 100644 index 0000000000..d93f696685 --- /dev/null +++ b/django/dispatch/dispatcher.py @@ -0,0 +1,497 @@ +"""Multiple-producer-multiple-consumer signal-dispatching + +dispatcher is the core of the PyDispatcher system, +providing the primary API and the core logic for the +system. + +Module attributes of note: + + Any -- Singleton used to signal either "Any Sender" or + "Any Signal". See documentation of the _Any class. + Anonymous -- Singleton used to signal "Anonymous Sender" + See documentation of the _Anonymous class. + +Internal attributes: + WEAKREF_TYPES -- tuple of types/classes which represent + weak references to receivers, and thus must be de- + referenced on retrieval to retrieve the callable + object + connections -- { senderkey (id) : { signal : [receivers...]}} + senders -- { senderkey (id) : weakref(sender) } + used for cleaning up sender references on sender + deletion + sendersBack -- { receiverkey (id) : [senderkey (id)...] } + used for cleaning up receiver references on receiver + deletion, (considerably speeds up the cleanup process + vs. the original code.) +""" +from __future__ import generators +import types, weakref +from django.dispatch import saferef, robustapply, errors + +__author__ = "Patrick K. O'Brien <pobrien@orbtech.com>" +__cvsid__ = "$Id: dispatcher.py,v 1.9 2005/09/17 04:55:57 mcfletch Exp $" +__version__ = "$Revision: 1.9 $"[11:-2] + +try: + True +except NameError: + True = 1==1 + False = 1==0 + +class _Parameter: + """Used to represent default parameter values.""" + def __repr__(self): + return self.__class__.__name__ + +class _Any(_Parameter): + """Singleton used to signal either "Any Sender" or "Any Signal" + + The Any object can be used with connect, disconnect, + send, or sendExact to signal that the parameter given + Any should react to all senders/signals, not just + a particular sender/signal. + """ +Any = _Any() + +class _Anonymous(_Parameter): + """Singleton used to signal "Anonymous Sender" + + The Anonymous object is used to signal that the sender + of a message is not specified (as distinct from being + "any sender"). Registering callbacks for Anonymous + will only receive messages sent without senders. Sending + with anonymous will only send messages to those receivers + registered for Any or Anonymous. + + Note: + The default sender for connect is Any, while the + default sender for send is Anonymous. This has + the effect that if you do not specify any senders + in either function then all messages are routed + as though there was a single sender (Anonymous) + being used everywhere. + """ +Anonymous = _Anonymous() + +WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref) + +connections = {} +senders = {} +sendersBack = {} + + +def connect(receiver, signal=Any, sender=Any, weak=True): + """Connect receiver to sender for signal + + receiver -- a callable Python object which is to receive + messages/signals/events. Receivers must be hashable + objects. + + if weak is True, then receiver must be weak-referencable + (more precisely saferef.safeRef() must be able to create + a reference to the receiver). + + Receivers are fairly flexible in their specification, + as the machinery in the robustApply module takes care + of most of the details regarding figuring out appropriate + subsets of the sent arguments to apply to a given + receiver. + + Note: + if receiver is itself a weak reference (a callable), + it will be de-referenced by the system's machinery, + so *generally* weak references are not suitable as + receivers, though some use might be found for the + facility whereby a higher-level library passes in + pre-weakrefed receiver references. + + signal -- the signal to which the receiver should respond + + if Any, receiver will receive any signal from the + indicated sender (which might also be Any, but is not + necessarily Any). + + Otherwise must be a hashable Python object other than + None (DispatcherError raised on None). + + sender -- the sender to which the receiver should respond + + if Any, receiver will receive the indicated signals + from any sender. + + if Anonymous, receiver will only receive indicated + signals from send/sendExact which do not specify a + sender, or specify Anonymous explicitly as the sender. + + Otherwise can be any python object. + + weak -- whether to use weak references to the receiver + By default, the module will attempt to use weak + references to the receiver objects. If this parameter + is false, then strong references will be used. + + returns None, may raise DispatcherTypeError + """ + if signal is None: + raise errors.DispatcherTypeError( + 'Signal cannot be None (receiver=%r sender=%r)'%( receiver,sender) + ) + if weak: + receiver = saferef.safeRef(receiver, onDelete=_removeReceiver) + senderkey = id(sender) + if connections.has_key(senderkey): + signals = connections[senderkey] + else: + connections[senderkey] = signals = {} + # Keep track of senders for cleanup. + # Is Anonymous something we want to clean up? + if sender not in (None, Anonymous, Any): + def remove(object, senderkey=senderkey): + _removeSender(senderkey=senderkey) + # Skip objects that can not be weakly referenced, which means + # they won't be automatically cleaned up, but that's too bad. + try: + weakSender = weakref.ref(sender, remove) + senders[senderkey] = weakSender + except: + pass + + receiverID = id(receiver) + # get current set, remove any current references to + # this receiver in the set, including back-references + if signals.has_key(signal): + receivers = signals[signal] + _removeOldBackRefs(senderkey, signal, receiver, receivers) + else: + receivers = signals[signal] = [] + try: + current = sendersBack.get( receiverID ) + if current is None: + sendersBack[ receiverID ] = current = [] + if senderkey not in current: + current.append(senderkey) + except: + pass + + receivers.append(receiver) + + + +def disconnect(receiver, signal=Any, sender=Any, weak=True): + """Disconnect receiver from sender for signal + + receiver -- the registered receiver to disconnect + signal -- the registered signal to disconnect + sender -- the registered sender to disconnect + weak -- the weakref state to disconnect + + disconnect reverses the process of connect, + the semantics for the individual elements are + logically equivalent to a tuple of + (receiver, signal, sender, weak) used as a key + to be deleted from the internal routing tables. + (The actual process is slightly more complex + but the semantics are basically the same). + + Note: + Using disconnect is not required to cleanup + routing when an object is deleted, the framework + will remove routes for deleted objects + automatically. It's only necessary to disconnect + if you want to stop routing to a live object. + + returns None, may raise DispatcherTypeError or + DispatcherKeyError + """ + if signal is None: + raise errors.DispatcherTypeError( + 'Signal cannot be None (receiver=%r sender=%r)'%( receiver,sender) + ) + if weak: receiver = saferef.safeRef(receiver) + senderkey = id(sender) + try: + signals = connections[senderkey] + receivers = signals[signal] + except KeyError: + raise errors.DispatcherKeyError( + """No receivers found for signal %r from sender %r""" %( + signal, + sender + ) + ) + try: + # also removes from receivers + _removeOldBackRefs(senderkey, signal, receiver, receivers) + except ValueError: + raise errors.DispatcherKeyError( + """No connection to receiver %s for signal %s from sender %s""" %( + receiver, + signal, + sender + ) + ) + _cleanupConnections(senderkey, signal) + +def getReceivers( sender = Any, signal = Any ): + """Get list of receivers from global tables + + This utility function allows you to retrieve the + raw list of receivers from the connections table + for the given sender and signal pair. + + Note: + there is no guarantee that this is the actual list + stored in the connections table, so the value + should be treated as a simple iterable/truth value + rather than, for instance a list to which you + might append new records. + + Normally you would use liveReceivers( getReceivers( ...)) + to retrieve the actual receiver objects as an iterable + object. + """ + try: + return connections[id(sender)][signal] + except KeyError: + return [] + +def liveReceivers(receivers): + """Filter sequence of receivers to get resolved, live receivers + + This is a generator which will iterate over + the passed sequence, checking for weak references + and resolving them, then returning all live + receivers. + """ + for receiver in receivers: + if isinstance( receiver, WEAKREF_TYPES): + # Dereference the weak reference. + receiver = receiver() + if receiver is not None: + yield receiver + else: + yield receiver + + + +def getAllReceivers( sender = Any, signal = Any ): + """Get list of all receivers from global tables + + This gets all receivers which should receive + the given signal from sender, each receiver should + be produced only once by the resulting generator + """ + receivers = {} + for set in ( + # Get receivers that receive *this* signal from *this* sender. + getReceivers( sender, signal ), + # Add receivers that receive *any* signal from *this* sender. + getReceivers( sender, Any ), + # Add receivers that receive *this* signal from *any* sender. + getReceivers( Any, signal ), + # Add receivers that receive *any* signal from *any* sender. + getReceivers( Any, Any ), + ): + for receiver in set: + if receiver: # filter out dead instance-method weakrefs + try: + if not receivers.has_key( receiver ): + receivers[receiver] = 1 + yield receiver + except TypeError: + # dead weakrefs raise TypeError on hash... + pass + +def send(signal=Any, sender=Anonymous, *arguments, **named): + """Send signal from sender to all connected receivers. + + signal -- (hashable) signal value, see connect for details + + sender -- the sender of the signal + + if Any, only receivers registered for Any will receive + the message. + + if Anonymous, only receivers registered to receive + messages from Anonymous or Any will receive the message + + Otherwise can be any python object (normally one + registered with a connect if you actually want + something to occur). + + arguments -- positional arguments which will be passed to + *all* receivers. Note that this may raise TypeErrors + if the receivers do not allow the particular arguments. + Note also that arguments are applied before named + arguments, so they should be used with care. + + named -- named arguments which will be filtered according + to the parameters of the receivers to only provide those + acceptable to the receiver. + + Return a list of tuple pairs [(receiver, response), ... ] + + if any receiver raises an error, the error propagates back + through send, terminating the dispatch loop, so it is quite + possible to not have all receivers called if a raises an + error. + """ + # Call each receiver with whatever arguments it can accept. + # Return a list of tuple pairs [(receiver, response), ... ]. + responses = [] + for receiver in liveReceivers(getAllReceivers(sender, signal)): + response = robustapply.robustApply( + receiver, + signal=signal, + sender=sender, + *arguments, + **named + ) + responses.append((receiver, response)) + return responses +def sendExact( signal=Any, sender=Anonymous, *arguments, **named ): + """Send signal only to those receivers registered for exact message + + sendExact allows for avoiding Any/Anonymous registered + handlers, sending only to those receivers explicitly + registered for a particular signal on a particular + sender. + """ + responses = [] + for receiver in liveReceivers(getReceivers(sender, signal)): + response = robustapply.robustApply( + receiver, + signal=signal, + sender=sender, + *arguments, + **named + ) + responses.append((receiver, response)) + return responses + + +def _removeReceiver(receiver): + """Remove receiver from connections.""" + if not sendersBack: + # During module cleanup the mapping will be replaced with None + return False + backKey = id(receiver) + for senderkey in sendersBack.get(backKey,()): + try: + signals = connections[senderkey].keys() + except KeyError,err: + pass + else: + for signal in signals: + try: + receivers = connections[senderkey][signal] + except KeyError: + pass + else: + try: + receivers.remove( receiver ) + except Exception, err: + pass + _cleanupConnections(senderkey, signal) + try: + del sendersBack[ backKey ] + except KeyError: + pass + +def _cleanupConnections(senderkey, signal): + """Delete any empty signals for senderkey. Delete senderkey if empty.""" + try: + receivers = connections[senderkey][signal] + except: + pass + else: + if not receivers: + # No more connected receivers. Therefore, remove the signal. + try: + signals = connections[senderkey] + except KeyError: + pass + else: + del signals[signal] + if not signals: + # No more signal connections. Therefore, remove the sender. + _removeSender(senderkey) + +def _removeSender(senderkey): + """Remove senderkey from connections.""" + _removeBackrefs(senderkey) + try: + del connections[senderkey] + except KeyError: + pass + # Senderkey will only be in senders dictionary if sender + # could be weakly referenced. + try: + del senders[senderkey] + except: + pass + + +def _removeBackrefs( senderkey): + """Remove all back-references to this senderkey""" + try: + signals = connections[senderkey] + except KeyError: + signals = None + else: + items = signals.items() + def allReceivers( ): + for signal,set in items: + for item in set: + yield item + for receiver in allReceivers(): + _killBackref( receiver, senderkey ) + +def _removeOldBackRefs(senderkey, signal, receiver, receivers): + """Kill old sendersBack references from receiver + + This guards against multiple registration of the same + receiver for a given signal and sender leaking memory + as old back reference records build up. + + Also removes old receiver instance from receivers + """ + try: + index = receivers.index(receiver) + # need to scan back references here and remove senderkey + except ValueError: + return False + else: + oldReceiver = receivers[index] + del receivers[index] + found = 0 + signals = connections.get(signal) + if signals is not None: + for sig,recs in connections.get(signal,{}).iteritems(): + if sig != signal: + for rec in recs: + if rec is oldReceiver: + found = 1 + break + if not found: + _killBackref( oldReceiver, senderkey ) + return True + return False + + +def _killBackref( receiver, senderkey ): + """Do the actual removal of back reference from receiver to senderkey""" + receiverkey = id(receiver) + set = sendersBack.get( receiverkey, () ) + while senderkey in set: + try: + set.remove( senderkey ) + except: + break + if not set: + try: + del sendersBack[ receiverkey ] + except KeyError: + pass + return True diff --git a/django/dispatch/errors.py b/django/dispatch/errors.py new file mode 100644 index 0000000000..a2eb32ed75 --- /dev/null +++ b/django/dispatch/errors.py @@ -0,0 +1,10 @@ +"""Error types for dispatcher mechanism +""" + +class DispatcherError(Exception): + """Base class for all Dispatcher errors""" +class DispatcherKeyError(KeyError, DispatcherError): + """Error raised when unknown (sender,signal) set specified""" +class DispatcherTypeError(TypeError, DispatcherError): + """Error raised when inappropriate signal-type specified (None)""" + diff --git a/django/dispatch/license.txt b/django/dispatch/license.txt new file mode 100644 index 0000000000..2f0b6b5ef2 --- /dev/null +++ b/django/dispatch/license.txt @@ -0,0 +1,34 @@ +PyDispatcher License + + Copyright (c) 2001-2003, Patrick K. O'Brien and Contributors + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + + The name of Patrick K. O'Brien, or the name of any Contributor, + may not be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/django/dispatch/robust.py b/django/dispatch/robust.py new file mode 100644 index 0000000000..ba19934d43 --- /dev/null +++ b/django/dispatch/robust.py @@ -0,0 +1,57 @@ +"""Module implementing error-catching version of send (sendRobust)""" +from django.dispatch.dispatcher import Any, Anonymous, liveReceivers, getAllReceivers +from django.dispatch.robustapply import robustApply + +def sendRobust( + signal=Any, + sender=Anonymous, + *arguments, **named +): + """Send signal from sender to all connected receivers catching errors + + signal -- (hashable) signal value, see connect for details + + sender -- the sender of the signal + + if Any, only receivers registered for Any will receive + the message. + + if Anonymous, only receivers registered to receive + messages from Anonymous or Any will receive the message + + Otherwise can be any python object (normally one + registered with a connect if you actually want + something to occur). + + arguments -- positional arguments which will be passed to + *all* receivers. Note that this may raise TypeErrors + if the receivers do not allow the particular arguments. + Note also that arguments are applied before named + arguments, so they should be used with care. + + named -- named arguments which will be filtered according + to the parameters of the receivers to only provide those + acceptable to the receiver. + + Return a list of tuple pairs [(receiver, response), ... ] + + if any receiver raises an error (specifically any subclass of Exception), + the error instance is returned as the result for that receiver. + """ + # Call each receiver with whatever arguments it can accept. + # Return a list of tuple pairs [(receiver, response), ... ]. + responses = [] + for receiver in liveReceivers(getAllReceivers(sender, signal)): + try: + response = robustApply( + receiver, + signal=signal, + sender=sender, + *arguments, + **named + ) + except Exception, err: + responses.append((receiver, err)) + else: + responses.append((receiver, response)) + return responses diff --git a/django/dispatch/robustapply.py b/django/dispatch/robustapply.py new file mode 100644 index 0000000000..0350e60cfc --- /dev/null +++ b/django/dispatch/robustapply.py @@ -0,0 +1,49 @@ +"""Robust apply mechanism + +Provides a function "call", which can sort out +what arguments a given callable object can take, +and subset the given arguments to match only +those which are acceptable. +""" + +def function( receiver ): + """Get function-like callable object for given receiver + + returns (function_or_method, codeObject, fromMethod) + + If fromMethod is true, then the callable already + has its first argument bound + """ + if hasattr(receiver, '__call__'): + # receiver is a class instance; assume it is callable. + # Reassign receiver to the actual method that will be called. + if hasattr( receiver.__call__, 'im_func') or hasattr( receiver.__call__, 'im_code'): + receiver = receiver.__call__ + if hasattr( receiver, 'im_func' ): + # an instance-method... + return receiver, receiver.im_func.func_code, 1 + elif not hasattr( receiver, 'func_code'): + raise ValueError('unknown reciever type %s %s'%(receiver, type(receiver))) + return receiver, receiver.func_code, 0 + +def robustApply(receiver, *arguments, **named): + """Call receiver with arguments and an appropriate subset of named + """ + receiver, codeObject, startIndex = function( receiver ) + acceptable = codeObject.co_varnames[startIndex+len(arguments):codeObject.co_argcount] + for name in codeObject.co_varnames[startIndex:startIndex+len(arguments)]: + if named.has_key( name ): + raise TypeError( + """Argument %r specified both positionally and as a keyword for calling %r"""% ( + name, receiver, + ) + ) + if not (codeObject.co_flags & 8): + # fc does not have a **kwds type parameter, therefore + # remove unacceptable arguments. + for arg in named.keys(): + if arg not in acceptable: + del named[arg] + return receiver(*arguments, **named) + +
\ No newline at end of file diff --git a/django/dispatch/saferef.py b/django/dispatch/saferef.py new file mode 100644 index 0000000000..6b3eda1d38 --- /dev/null +++ b/django/dispatch/saferef.py @@ -0,0 +1,165 @@ +"""Refactored "safe reference" from dispatcher.py""" +import weakref, traceback + +def safeRef(target, onDelete = None): + """Return a *safe* weak reference to a callable target + + target -- the object to be weakly referenced, if it's a + bound method reference, will create a BoundMethodWeakref, + otherwise creates a simple weakref. + onDelete -- if provided, will have a hard reference stored + to the callable to be called after the safe reference + goes out of scope with the reference object, (either a + weakref or a BoundMethodWeakref) as argument. + """ + if hasattr(target, 'im_self'): + if target.im_self is not None: + # Turn a bound method into a BoundMethodWeakref instance. + # Keep track of these instances for lookup by disconnect(). + assert hasattr(target, 'im_func'), """safeRef target %r has im_self, but no im_func, don't know how to create reference"""%( target,) + reference = BoundMethodWeakref( + target=target, + onDelete=onDelete + ) + return reference + if callable(onDelete): + return weakref.ref(target, onDelete) + else: + return weakref.ref( target ) + +class BoundMethodWeakref(object): + """'Safe' and reusable weak references to instance methods + + BoundMethodWeakref objects provide a mechanism for + referencing a bound method without requiring that the + method object itself (which is normally a transient + object) is kept alive. Instead, the BoundMethodWeakref + object keeps weak references to both the object and the + function which together define the instance method. + + Attributes: + key -- the identity key for the reference, calculated + by the class's calculateKey method applied to the + target instance method + deletionMethods -- sequence of callable objects taking + single argument, a reference to this object which + will be called when *either* the target object or + target function is garbage collected (i.e. when + this object becomes invalid). These are specified + as the onDelete parameters of safeRef calls. + weakSelf -- weak reference to the target object + weakFunc -- weak reference to the target function + + Class Attributes: + _allInstances -- class attribute pointing to all live + BoundMethodWeakref objects indexed by the class's + calculateKey(target) method applied to the target + objects. This weak value dictionary is used to + short-circuit creation so that multiple references + to the same (object, function) pair produce the + same BoundMethodWeakref instance. + + """ + _allInstances = weakref.WeakValueDictionary() + def __new__( cls, target, onDelete=None, *arguments,**named ): + """Create new instance or return current instance + + Basically this method of construction allows us to + short-circuit creation of references to already- + referenced instance methods. The key corresponding + to the target is calculated, and if there is already + an existing reference, that is returned, with its + deletionMethods attribute updated. Otherwise the + new instance is created and registered in the table + of already-referenced methods. + """ + key = cls.calculateKey(target) + current =cls._allInstances.get(key) + if current is not None: + current.deletionMethods.append( onDelete) + return current + else: + base = super( BoundMethodWeakref, cls).__new__( cls ) + cls._allInstances[key] = base + base.__init__( target, onDelete, *arguments,**named) + return base + def __init__(self, target, onDelete=None): + """Return a weak-reference-like instance for a bound method + + target -- the instance-method target for the weak + reference, must have im_self and im_func attributes + and be reconstructable via: + target.im_func.__get__( target.im_self ) + which is true of built-in instance methods. + onDelete -- optional callback which will be called + when this weak reference ceases to be valid + (i.e. either the object or the function is garbage + collected). Should take a single argument, + which will be passed a pointer to this object. + """ + def remove(weak, self=self): + """Set self.isDead to true when method or instance is destroyed""" + methods = self.deletionMethods[:] + del self.deletionMethods[:] + try: + del self.__class__._allInstances[ self.key ] + except KeyError: + pass + for function in methods: + try: + if callable( function ): + function( self ) + except Exception, e: + try: + traceback.print_exc() + except AttributeError, err: + print '''Exception during saferef %s cleanup function %s: %s'''%( + self, function, e + ) + self.deletionMethods = [onDelete] + self.key = self.calculateKey( target ) + self.weakSelf = weakref.ref(target.im_self, remove) + self.weakFunc = weakref.ref(target.im_func, remove) + self.selfName = str(target.im_self) + self.funcName = str(target.im_func.__name__) + def calculateKey( cls, target ): + """Calculate the reference key for this reference + + Currently this is a two-tuple of the id()'s of the + target object and the target function respectively. + """ + return (id(target.im_self),id(target.im_func)) + calculateKey = classmethod( calculateKey ) + def __str__(self): + """Give a friendly representation of the object""" + return """%s( %s.%s )"""%( + self.__class__.__name__, + self.selfName, + self.funcName, + ) + __repr__ = __str__ + def __nonzero__( self ): + """Whether we are still a valid reference""" + return self() is not None + def __cmp__( self, other ): + """Compare with another reference""" + if not isinstance (other,self.__class__): + return cmp( self.__class__, type(other) ) + return cmp( self.key, other.key) + def __call__(self): + """Return a strong reference to the bound method + + If the target cannot be retrieved, then will + return None, otherwise returns a bound instance + method for our object and function. + + Note: + You may call this method any number of times, + as it does not invalidate the reference. + """ + target = self.weakSelf() + if target is not None: + function = self.weakFunc() + if function is not None: + return function.__get__(target) + return None diff --git a/django/core/formfields.py b/django/forms/__init__.py index 167439cc07..b0c1c2004f 100644 --- a/django/core/formfields.py +++ b/django/forms/__init__.py @@ -1,8 +1,8 @@ from django.core import validators from django.core.exceptions import PermissionDenied from django.utils.html import escape -from django.conf.settings import DEFAULT_CHARSET -from django.utils.translation import gettext_lazy, ngettext +from django.conf import settings +from django.utils.translation import gettext, gettext_lazy, ngettext FORM_FIELD_ID_PREFIX = 'id_' @@ -10,7 +10,7 @@ class EmptyValue(Exception): "This is raised when empty data is provided" pass -class Manipulator: +class Manipulator(object): # List of permission strings. User must have at least one to manipulate. # None means everybody has permission. required_permission = '' @@ -55,26 +55,35 @@ class Manipulator: "Returns dictionary mapping field_names to error-message lists" errors = {} for field in self.fields: - if field.is_required and not new_data.get(field.field_name, False): - errors.setdefault(field.field_name, []).append(gettext_lazy('This field is required.')) - continue - try: - validator_list = field.validator_list - if hasattr(self, 'validate_%s' % field.field_name): - validator_list.append(getattr(self, 'validate_%s' % field.field_name)) - for validator in validator_list: - if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'): - try: - if hasattr(field, 'requires_data_list'): - validator(new_data.getlist(field.field_name), new_data) - else: - validator(new_data.get(field.field_name, ''), new_data) - except validators.ValidationError, e: - errors.setdefault(field.field_name, []).extend(e.messages) - # If a CriticalValidationError is raised, ignore any other ValidationErrors - # for this particular field - except validators.CriticalValidationError, e: - errors.setdefault(field.field_name, []).extend(e.messages) + errors.update(field.get_validation_errors(new_data)) + val_name = 'validate_%s' % field.field_name + if hasattr(self, val_name): + val = getattr(self, val_name) + try: + field.run_validator(new_data, val) + except (validators.ValidationError, validators.CriticalValidationError), e: + errors.setdefault(field.field_name, []).extend(e.messages) + +# if field.is_required and not new_data.get(field.field_name, False): +# errors.setdefault(field.field_name, []).append(gettext_lazy('This field is required.')) +# continue +# try: +# validator_list = field.validator_list +# if hasattr(self, 'validate_%s' % field.field_name): +# validator_list.append(getattr(self, 'validate_%s' % field.field_name)) +# for validator in validator_list: +# if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'): +# try: +# if hasattr(field, 'requires_data_list'): +# validator(new_data.getlist(field.field_name), new_data) +# else: +# validator(new_data.get(field.field_name, ''), new_data) +# except validators.ValidationError, e: +# errors.setdefault(field.field_name, []).extend(e.messages) +# # If a CriticalValidationError is raised, ignore any other ValidationErrors +# # for this particular field +# except validators.CriticalValidationError, e: +# errors.setdefault(field.field_name, []).extend(e.messages) return errors def save(self, new_data): @@ -203,7 +212,7 @@ class FormFieldCollection(FormFieldWrapper): return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]) class InlineObjectCollection: - "An object that acts like a list of form field collections." + "An object that acts like a sparse list of form field collections." def __init__(self, parent_manipulator, rel_obj, data, errors): self.parent_manipulator = parent_manipulator self.rel_obj = rel_obj @@ -230,27 +239,35 @@ class InlineObjectCollection: def __iter__(self): self.fill() - return self._collections.__iter__() + return iter(self._collections.values()) + + def items(self): + self.fill() + return self._collections.items() def fill(self): if self._collections: return else: var_name = self.rel_obj.opts.object_name.lower() - wrapper = [] - orig = hasattr(self.parent_manipulator, 'original_object') and self.parent_manipulator.original_object or None + collections = {} + orig = None + if hasattr(self.parent_manipulator, 'original_object'): + orig = self.parent_manipulator.original_object orig_list = self.rel_obj.get_list(orig) + for i, instance in enumerate(orig_list): collection = {'original': instance} for f in self.rel_obj.editable_fields(): - for field_name in f.get_manipulator_field_names(''): - full_field_name = '%s.%d.%s' % (var_name, i, field_name) - field = self.parent_manipulator[full_field_name] - data = field.extract_data(self.data) - errors = self.errors.get(full_field_name, []) - collection[field_name] = FormFieldWrapper(field, data, errors) - wrapper.append(FormFieldCollection(collection)) - self._collections = wrapper + for field_name in f.get_manipulator_field_names(''): + full_field_name = '%s.%d.%s' % (var_name, i, field_name) + field = self.parent_manipulator[full_field_name] + data = field.extract_data(self.data) + errors = self.errors.get(full_field_name, []) + collection[field_name] = FormFieldWrapper(field, data, errors) + collections[i] = FormFieldCollection(collection) + self._collections = collections + class FormField: """Abstract class representing a form field. @@ -310,10 +327,35 @@ class FormField: new_data.setlist(name, converted_data) else: try: - # individual fields deal with None values themselves - new_data.setlist(name, [self.__class__.html2python(None)]) + #individual fields deal with None values themselves + new_data.setlist(name, [self.__class__.html2python(None)]) except EmptyValue: - new_data.setlist(name, []) + new_data.setlist(name, []) + + + def run_validator(self, new_data, validator): + if self.is_required or new_data.get(self.field_name, False) or hasattr(validator, 'always_test'): + if hasattr(self, 'requires_data_list'): + validator(new_data.getlist(self.field_name), new_data) + else: + validator(new_data.get(self.field_name, ''), new_data) + + def get_validation_errors(self, new_data): + errors = {} + if self.is_required and not new_data.get(self.field_name, False): + errors.setdefault(self.field_name, []).append(gettext_lazy('This field is required.')) + return errors + try: + for validator in self.validator_list: + try: + self.run_validator(new_data, validator) + except validators.ValidationError, e: + errors.setdefault(self.field_name, []).extend(e.messages) + # If a CriticalValidationError is raised, ignore any other ValidationErrors + # for this particular field + except validators.CriticalValidationError, e: + errors.setdefault(self.field_name, []).extend(e.messages) + return errors def get_id(self): "Returns the HTML 'id' attribute for this form field." @@ -334,13 +376,13 @@ class TextField(FormField): self.member_name = member_name def isValidLength(self, data, form): - if data and self.maxlength and len(data.decode(DEFAULT_CHARSET)) > self.maxlength: + if data and self.maxlength and len(data.decode(settings.DEFAULT_CHARSET)) > self.maxlength: raise validators.ValidationError, ngettext("Ensure your text is less than %s character.", "Ensure your text is less than %s characters.", self.maxlength) % self.maxlength def hasNoNewlines(self, data, form): if data and '\n' in data: - raise validators.ValidationError, _("Line breaks are not allowed here.") + raise validators.ValidationError, gettext("Line breaks are not allowed here.") def render(self, data): if data is None: @@ -349,7 +391,7 @@ class TextField(FormField): if self.maxlength: maxlength = 'maxlength="%s" ' % self.maxlength if isinstance(data, unicode): - data = data.encode(DEFAULT_CHARSET) + data = data.encode(settings.DEFAULT_CHARSET) return '<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \ (self.input_type, self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', self.field_name, self.length, escape(data), maxlength) @@ -374,7 +416,7 @@ class LargeTextField(TextField): if data is None: data = '' if isinstance(data, unicode): - data = data.encode(DEFAULT_CHARSET) + data = data.encode(settings.DEFAULT_CHARSET) return '<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \ (self.get_id(), self.__class__.__name__, self.is_required and ' required' or '', self.field_name, self.rows, self.cols, escape(data)) @@ -435,7 +477,7 @@ class SelectField(FormField): str_data = str(data) str_choices = [str(item[0]) for item in self.choices] if str_data not in str_choices: - raise validators.ValidationError, _("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data': str_data, 'choices': str_choices} + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data': str_data, 'choices': str_choices} class NullSelectField(SelectField): "This SelectField converts blank fields to None" @@ -506,7 +548,7 @@ class RadioSelectField(FormField): str_data = str(data) str_choices = [str(item[0]) for item in self.choices] if str_data not in str_choices: - raise validators.ValidationError, _("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices} + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':str_data, 'choices':str_choices} class NullBooleanField(SelectField): "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None" @@ -544,7 +586,7 @@ class SelectMultipleField(SelectField): str_choices = [str(item[0]) for item in self.choices] for val in map(str, field_data): if val not in str_choices: - raise validators.ValidationError, _("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':val, 'choices':str_choices} + raise validators.ValidationError, gettext("Select a valid choice; '%(data)s' is not in %(choices)s.") % {'data':val, 'choices':str_choices} def html2python(data): if data is None: @@ -600,7 +642,7 @@ class FileUploadField(FormField): def isNonEmptyFile(self, field_data, all_data): if not field_data['content']: - raise validators.CriticalValidationError, _("The submitted file is empty.") + raise validators.CriticalValidationError, gettext("The submitted file is empty.") def render(self, data): return '<input type="file" id="%s" class="v%s" name="%s" />' % \ @@ -654,7 +696,7 @@ class SmallIntegerField(IntegerField): def isSmallInteger(self, field_data, all_data): if not -32768 <= int(field_data) <= 32767: - raise validators.CriticalValidationError, _("Enter a whole number between -32,768 and 32,767.") + raise validators.CriticalValidationError, gettext("Enter a whole number between -32,768 and 32,767.") class PositiveIntegerField(IntegerField): def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]): @@ -663,7 +705,7 @@ class PositiveIntegerField(IntegerField): def isPositive(self, field_data, all_data): if int(field_data) < 0: - raise validators.CriticalValidationError, _("Enter a positive number.") + raise validators.CriticalValidationError, gettext("Enter a positive number.") class PositiveSmallIntegerField(IntegerField): def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=[]): @@ -672,7 +714,7 @@ class PositiveSmallIntegerField(IntegerField): def isPositiveSmall(self, field_data, all_data): if not 0 <= int(field_data) <= 32767: - raise validators.CriticalValidationError, _("Enter a whole number between 0 and 32,767.") + raise validators.CriticalValidationError, gettext("Enter a whole number between 0 and 32,767.") class FloatField(TextField): def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=[]): @@ -741,7 +783,7 @@ class DateField(TextField): time_tuple = time.strptime(data, '%Y-%m-%d') return datetime.date(*time_tuple[0:3]) except (ValueError, TypeError): - return None + return data html2python = staticmethod(html2python) class TimeField(TextField): @@ -837,7 +879,7 @@ class FilePathField(SelectField): for root, dirs, files in os.walk(path): for f in files: if match is None or match_re.search(f): - choices.append((os.path.join(path, f), f)) + choices.append((os.path.join(root, f), f)) else: try: for f in os.listdir(path): diff --git a/django/utils/httpwrappers.py b/django/http/__init__.py index c059ff60a8..bb03ab8ea5 100644 --- a/django/utils/httpwrappers.py +++ b/django/http/__init__.py @@ -9,6 +9,9 @@ try: except ImportError: from cgi import parse_qsl +class Http404(Exception): + pass + class HttpRequest(object): # needs to be new-style class because subclasses define "property"s "A basic HTTP request" def __init__(self): @@ -271,3 +274,10 @@ class HttpResponseServerError(HttpResponse): def __init__(self, *args, **kwargs): HttpResponse.__init__(self, *args, **kwargs) self.status_code = 500 + +def get_host(request): + """Gets the HTTP host from the environment or request headers.""" + host = request.META.get('HTTP_X_FORWARDED_HOST', '') + if not host: + host = request.META.get('HTTP_HOST', '') + return host diff --git a/django/middleware/cache.py b/django/middleware/cache.py index bb3396c849..b5e142a383 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -1,7 +1,7 @@ from django.conf import settings from django.core.cache import cache from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers -from django.utils.httpwrappers import HttpResponseNotModified +from django.http import HttpResponseNotModified class CacheMiddleware: """ diff --git a/django/middleware/common.py b/django/middleware/common.py index 3643fce553..763918878a 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.utils import httpwrappers +from django import http from django.core.mail import mail_managers import md5, os @@ -27,10 +27,11 @@ class CommonMiddleware: if request.META.has_key('HTTP_USER_AGENT'): for user_agent_regex in settings.DISALLOWED_USER_AGENTS: if user_agent_regex.search(request.META['HTTP_USER_AGENT']): - return httpwrappers.HttpResponseForbidden('<h1>Forbidden</h1>') + return http.HttpResponseForbidden('<h1>Forbidden</h1>') # Check for a redirect based on settings.APPEND_SLASH and settings.PREPEND_WWW - old_url = [request.META.get('HTTP_HOST', ''), request.path] + host = http.get_host(request) + old_url = [host, request.path] new_url = old_url[:] if settings.PREPEND_WWW and old_url[0] and not old_url[0].startswith('www.'): new_url[0] = 'www.' + old_url[0] @@ -46,7 +47,7 @@ class CommonMiddleware: newurl = new_url[1] if request.GET: newurl += '?' + request.GET.urlencode() - return httpwrappers.HttpResponsePermanentRedirect(newurl) + return http.HttpResponsePermanentRedirect(newurl) return None @@ -56,7 +57,7 @@ class CommonMiddleware: if settings.SEND_BROKEN_LINK_EMAILS: # If the referrer was from an internal link or a non-search-engine site, # send a note to the managers. - domain = request.META['HTTP_HOST'] + domain = http.get_host(request) referer = request.META.get('HTTP_REFERER', None) is_internal = referer and (domain in referer) path = request.get_full_path() @@ -69,7 +70,7 @@ class CommonMiddleware: if settings.USE_ETAGS: etag = md5.new(response.content).hexdigest() if request.META.get('HTTP_IF_NONE_MATCH') == etag: - response = httpwrappers.HttpResponseNotModified() + response = http.HttpResponseNotModified() else: response['ETag'] = etag diff --git a/django/middleware/doc.py b/django/middleware/doc.py index c96be2c0f6..6376fe4d5c 100644 --- a/django/middleware/doc.py +++ b/django/middleware/doc.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.utils import httpwrappers +from django import http class XViewMiddleware: """ @@ -12,6 +12,6 @@ class XViewMiddleware: documentation module to lookup the view function for an arbitrary page. """ if request.META['REQUEST_METHOD'] == 'HEAD' and request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: - response = httpwrappers.HttpResponse() + response = http.HttpResponse() response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) return response diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py new file mode 100644 index 0000000000..da218ac31a --- /dev/null +++ b/django/middleware/transaction.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.db import transaction + +class TransactionMiddleware: + """ + Transaction middleware. If this is enabled, each view function will be run + with commit_on_response activated - that way a save() doesn't do a direct + commit, the commit is done when a successful response is created. If an + exception happens, the database is rolled back. + """ + def process_request(self, request): + """Enters transaction management""" + transaction.enter_transaction_management() + transaction.managed(True) + + def process_exception(self, request, exception): + """Rolls back the database and leaves transaction management""" + if transaction.is_dirty(): + transaction.rollback() + transaction.leave_transaction_management() + + def process_response(self, request, response): + """Commits and leaves transaction management.""" + if transaction.is_managed(): + if transaction.is_dirty(): + transaction.commit() + transaction.leave_transaction_management() + return response diff --git a/django/models/__init__.py b/django/models/__init__.py deleted file mode 100644 index ea0c66f2ae..0000000000 --- a/django/models/__init__.py +++ /dev/null @@ -1,95 +0,0 @@ -from django.core import meta -from django.utils.functional import curry - -__all__ = ['auth', 'core'] - -# Alter this package's __path__ variable so that calling code can import models -# from "django.models" even though the model code doesn't physically live -# within django.models. -for mod in meta.get_installed_models(): - __path__.extend(mod.__path__) - -# First, import all models so the metaclasses run. -modules = meta.get_installed_model_modules(__all__) - -# Now, create the extra methods that we couldn't create earlier because -# relationships hadn't been known until now. -for mod in modules: - for klass in mod._MODELS: - - # Add "get_thingie", "get_thingie_count" and "get_thingie_list" methods - # for all related objects. - for related in klass._meta.get_all_related_objects(): - # Determine whether this related object is in another app. - # If it's in another app, the method names will have the app - # label prepended, and the add_BLAH() method will not be - # generated. - rel_mod = related.opts.get_model_module() - rel_obj_name = related.get_method_name_part() - if isinstance(related.field.rel, meta.OneToOneRel): - # Add "get_thingie" methods for one-to-one related objects. - # EXAMPLE: Place.get_restaurants_restaurant() - func = curry(meta.method_get_related, 'get_object', rel_mod, related.field) - func.__doc__ = "Returns the associated `%s.%s` object." % (related.opts.app_label, related.opts.module_name) - setattr(klass, 'get_%s' % rel_obj_name, func) - elif isinstance(related.field.rel, meta.ManyToOneRel): - # Add "get_thingie" methods for many-to-one related objects. - # EXAMPLE: Poll.get_choice() - func = curry(meta.method_get_related, 'get_object', rel_mod, related.field) - func.__doc__ = "Returns the associated `%s.%s` object matching the given criteria." % \ - (related.opts.app_label, related.opts.module_name) - setattr(klass, 'get_%s' % rel_obj_name, func) - # Add "get_thingie_count" methods for many-to-one related objects. - # EXAMPLE: Poll.get_choice_count() - func = curry(meta.method_get_related, 'get_count', rel_mod, related.field) - func.__doc__ = "Returns the number of associated `%s.%s` objects." % \ - (related.opts.app_label, related.opts.module_name) - setattr(klass, 'get_%s_count' % rel_obj_name, func) - # Add "get_thingie_list" methods for many-to-one related objects. - # EXAMPLE: Poll.get_choice_list() - func = curry(meta.method_get_related, 'get_list', rel_mod, related.field) - func.__doc__ = "Returns a list of associated `%s.%s` objects." % \ - (related.opts.app_label, related.opts.module_name) - setattr(klass, 'get_%s_list' % rel_obj_name, func) - # Add "add_thingie" methods for many-to-one related objects, - # but only for related objects that are in the same app. - # EXAMPLE: Poll.add_choice() - if related.opts.app_label == klass._meta.app_label: - func = curry(meta.method_add_related, related.opts, rel_mod, related.field) - func.alters_data = True - setattr(klass, 'add_%s' % rel_obj_name, func) - del func - del rel_obj_name, rel_mod, related # clean up - - # Do the same for all related many-to-many objects. - for related in klass._meta.get_all_related_many_to_many_objects(): - rel_mod = related.opts.get_model_module() - rel_obj_name = related.get_method_name_part() - setattr(klass, 'get_%s' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_object', klass._meta, rel_mod, related.field)) - setattr(klass, 'get_%s_count' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_count', klass._meta, rel_mod, related.field)) - setattr(klass, 'get_%s_list' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_list', klass._meta, rel_mod, related.field)) - if related.opts.app_label == klass._meta.app_label: - func = curry(meta.method_set_related_many_to_many, related.opts, related.field) - func.alters_data = True - setattr(klass, 'set_%s' % related.opts.module_name, func) - del func - del rel_obj_name, rel_mod, related # clean up - - # Add "set_thingie_order" and "get_thingie_order" methods for objects - # that are ordered with respect to this. - for obj in klass._meta.get_ordered_objects(): - func = curry(meta.method_set_order, obj) - func.__doc__ = "Sets the order of associated `%s.%s` objects to the given ID list." % (obj.app_label, obj.module_name) - func.alters_data = True - setattr(klass, 'set_%s_order' % obj.object_name.lower(), func) - - func = curry(meta.method_get_order, obj) - func.__doc__ = "Returns the order of associated `%s.%s` objects as a list of IDs." % (obj.app_label, obj.module_name) - setattr(klass, 'get_%s_order' % obj.object_name.lower(), func) - del func, obj # clean up - del klass # clean up - del mod -del modules - -# Expose get_app and get_module. -from django.core.meta import get_app, get_module diff --git a/django/models/auth.py b/django/models/auth.py deleted file mode 100644 index 2595727ad0..0000000000 --- a/django/models/auth.py +++ /dev/null @@ -1,219 +0,0 @@ -from django.core import meta, validators -from django.models import core -from django.utils.translation import gettext_lazy as _ - -class Permission(meta.Model): - name = meta.CharField(_('name'), maxlength=50) - package = meta.ForeignKey(core.Package, db_column='package') - codename = meta.CharField(_('codename'), maxlength=100) - class META: - verbose_name = _('Permission') - verbose_name_plural = _('Permissions') - unique_together = (('package', 'codename'),) - ordering = ('package', 'codename') - - def __repr__(self): - return "%s | %s" % (self.package_id, self.name) - -class Group(meta.Model): - name = meta.CharField(_('name'), maxlength=80, unique=True) - permissions = meta.ManyToManyField(Permission, blank=True, filter_interface=meta.HORIZONTAL) - class META: - verbose_name = _('Group') - verbose_name_plural = _('Groups') - ordering = ('name',) - admin = meta.Admin( - search_fields = ('name',), - ) - - def __repr__(self): - return self.name - -class User(meta.Model): - username = meta.CharField(_('username'), maxlength=30, unique=True, validator_list=[validators.isAlphaNumeric]) - first_name = meta.CharField(_('first name'), maxlength=30, blank=True) - last_name = meta.CharField(_('last name'), maxlength=30, blank=True) - email = meta.EmailField(_('e-mail address'), blank=True) - password = meta.CharField(_('password'), maxlength=128, help_text=_("Use '[algo]$[salt]$[hexdigest]'")) - is_staff = meta.BooleanField(_('staff status'), help_text=_("Designates whether the user can log into this admin site.")) - is_active = meta.BooleanField(_('active'), default=True) - is_superuser = meta.BooleanField(_('superuser status')) - last_login = meta.DateTimeField(_('last login'), default=meta.LazyDate()) - date_joined = meta.DateTimeField(_('date joined'), default=meta.LazyDate()) - groups = meta.ManyToManyField(Group, blank=True, - help_text=_("In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in.")) - user_permissions = meta.ManyToManyField(Permission, blank=True, filter_interface=meta.HORIZONTAL) - class META: - verbose_name = _('User') - verbose_name_plural = _('Users') - module_constants = { - 'SESSION_KEY': '_auth_user_id', - } - ordering = ('username',) - exceptions = ('SiteProfileNotAvailable',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), - (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), - (_('Groups'), {'fields': ('groups',)}), - ), - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff'), - list_filter = ('is_staff', 'is_superuser'), - search_fields = ('username', 'first_name', 'last_name', 'email'), - ) - - def __repr__(self): - return self.username - - def get_absolute_url(self): - return "/users/%s/" % self.username - - def is_anonymous(self): - return False - - def get_full_name(self): - full_name = '%s %s' % (self.first_name, self.last_name) - return full_name.strip() - - def set_password(self, raw_password): - import sha, random - algo = 'sha1' - salt = sha.new(str(random.random())).hexdigest()[:5] - hsh = sha.new(salt+raw_password).hexdigest() - self.password = '%s$%s$%s' % (algo, salt, hsh) - - def check_password(self, raw_password): - """ - Returns a boolean of whether the raw_password was correct. Handles - encryption formats behind the scenes. - """ - # Backwards-compatibility check. Older passwords won't include the - # algorithm or salt. - if '$' not in self.password: - import md5 - is_correct = (self.password == md5.new(raw_password).hexdigest()) - if is_correct: - # Convert the password to the new, more secure format. - self.set_password(raw_password) - self.save() - return is_correct - algo, salt, hsh = self.password.split('$') - if algo == 'md5': - import md5 - return hsh == md5.new(salt+raw_password).hexdigest() - elif algo == 'sha1': - import sha - return hsh == sha.new(salt+raw_password).hexdigest() - raise ValueError, "Got unknown password algorithm type in password." - - def get_group_permissions(self): - "Returns a list of permission strings that this user has through his/her groups." - if not hasattr(self, '_group_perm_cache'): - import sets - cursor = db.cursor() - # The SQL below works out to the following, after DB quoting: - # cursor.execute(""" - # SELECT p.package, p.codename - # FROM auth_permissions p, auth_groups_permissions gp, auth_users_groups ug - # WHERE p.id = gp.permission_id - # AND gp.group_id = ug.group_id - # AND ug.user_id = %s""", [self.id]) - sql = """ - SELECT p.%s, p.%s - FROM %s p, %s gp, %s ug - WHERE p.%s = gp.%s - AND gp.%s = ug.%s - AND ug.%s = %%s""" % ( - db.quote_name('package'), db.quote_name('codename'), - db.quote_name('auth_permissions'), db.quote_name('auth_groups_permissions'), - db.quote_name('auth_users_groups'), db.quote_name('id'), - db.quote_name('permission_id'), db.quote_name('group_id'), - db.quote_name('group_id'), db.quote_name('user_id')) - cursor.execute(sql, [self.id]) - self._group_perm_cache = sets.Set(["%s.%s" % (row[0], row[1]) for row in cursor.fetchall()]) - return self._group_perm_cache - - def get_all_permissions(self): - if not hasattr(self, '_perm_cache'): - import sets - self._perm_cache = sets.Set(["%s.%s" % (p.package_id, p.codename) for p in self.get_permission_list()]) - self._perm_cache.update(self.get_group_permissions()) - return self._perm_cache - - def has_perm(self, perm): - "Returns True if the user has the specified permission." - if not self.is_active: - return False - if self.is_superuser: - return True - return perm in self.get_all_permissions() - - def has_perms(self, perm_list): - "Returns True if the user has each of the specified permissions." - for perm in perm_list: - if not self.has_perm(perm): - return False - return True - - def has_module_perms(self, package_name): - "Returns True if the user has any permissions in the given package." - if self.is_superuser: - return True - return bool(len([p for p in self.get_all_permissions() if p[:p.index('.')] == package_name])) - - def get_and_delete_messages(self): - messages = [] - for m in self.get_message_list(): - messages.append(m.message) - m.delete() - return messages - - def email_user(self, subject, message, from_email=None): - "Sends an e-mail to this User." - from django.core.mail import send_mail - send_mail(subject, message, from_email, [self.email]) - - def get_profile(self): - """ - Returns site-specific profile for this user. Raises - SiteProfileNotAvailable if this site does not allow profiles. - """ - if not hasattr(self, '_profile_cache'): - from django.conf.settings import AUTH_PROFILE_MODULE - if not AUTH_PROFILE_MODULE: - raise SiteProfileNotAvailable - try: - app, mod = AUTH_PROFILE_MODULE.split('.') - module = __import__('ellington.%s.apps.%s' % (app, mod), [], [], ['']) - self._profile_cache = module.get_object(user_id=self.id) - except ImportError: - try: - module = __import__('django.models.%s' % AUTH_PROFILE_MODULE, [], [], ['']) - self._profile_cache = module.get_object(user__id__exact=self.id) - except ImportError: - raise SiteProfileNotAvailable - return self._profile_cache - - def _module_create_user(username, email, password): - "Creates and saves a User with the given username, e-mail and password." - now = datetime.datetime.now() - user = User(None, username, '', '', email.strip().lower(), 'placeholder', False, True, False, now, now) - user.set_password(password) - user.save() - return user - - def _module_make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'): - "Generates a random password with the given length and given allowed_chars" - # Note that default value of allowed_chars does not have "I" or letters - # that look like it -- just to avoid confusion. - from random import choice - return ''.join([choice(allowed_chars) for i in range(length)]) - -class Message(meta.Model): - user = meta.ForeignKey(User) - message = meta.TextField(_('Message')) - - def __repr__(self): - return self.message diff --git a/django/models/core.py b/django/models/core.py deleted file mode 100644 index f78f23f265..0000000000 --- a/django/models/core.py +++ /dev/null @@ -1,121 +0,0 @@ -import base64, md5, random, sys -import cPickle as pickle -from django.core import meta -from django.utils.translation import gettext_lazy as _ - -class Site(meta.Model): - domain = meta.CharField(_('domain name'), maxlength=100) - name = meta.CharField(_('display name'), maxlength=50) - class META: - verbose_name = _('site') - verbose_name_plural = _('sites') - db_table = 'sites' - ordering = ('domain',) - admin = meta.Admin( - list_display = ('domain', 'name'), - search_fields = ('domain', 'name'), - ) - - def __repr__(self): - return self.domain - - def _module_get_current(): - "Returns the current site, according to the SITE_ID constant." - from django.conf.settings import SITE_ID - return get_object(pk=SITE_ID) - -class Package(meta.Model): - label = meta.CharField(_('label'), maxlength=20, primary_key=True) - name = meta.CharField(_('name'), maxlength=30, unique=True) - class META: - verbose_name = _('package') - verbose_name_plural = _('packages') - db_table = 'packages' - ordering = ('name',) - - def __repr__(self): - return self.name - -class ContentType(meta.Model): - name = meta.CharField(_('name'), maxlength=100) - package = meta.ForeignKey(Package, db_column='package') - python_module_name = meta.CharField(_('python module name'), maxlength=50) - class META: - verbose_name = _('content type') - verbose_name_plural = _('content types') - db_table = 'content_types' - ordering = ('package', 'name') - unique_together = (('package', 'python_module_name'),) - - def __repr__(self): - return "%s | %s" % (self.package_id, self.name) - - def get_model_module(self): - "Returns the Python model module for accessing this type of content." - return __import__('django.models.%s.%s' % (self.package_id, self.python_module_name), '', '', ['']) - - def get_object_for_this_type(self, **kwargs): - """ - Returns an object of this type for the keyword arguments given. - Basically, this is a proxy around this object_type's get_object() model - method. The ObjectNotExist exception, if thrown, will not be caught, - so code that calls this method should catch it. - """ - return self.get_model_module().get_object(**kwargs) - -class Session(meta.Model): - session_key = meta.CharField(_('session key'), maxlength=40, primary_key=True) - session_data = meta.TextField(_('session data')) - expire_date = meta.DateTimeField(_('expire date')) - class META: - verbose_name = _('session') - verbose_name_plural = _('sessions') - module_constants = { - 'base64': base64, - 'md5': md5, - 'pickle': pickle, - 'random': random, - 'sys': sys, - } - - def get_decoded(self): - from django.conf.settings import SECRET_KEY - encoded_data = base64.decodestring(self.session_data) - pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] - if md5.new(pickled + SECRET_KEY).hexdigest() != tamper_check: - from django.core.exceptions import SuspiciousOperation - raise SuspiciousOperation, "User tampered with session cookie." - try: - return pickle.loads(pickled) - # Unpickling can cause a variety of exceptions. If something happens, - # just return an empty dictionary (an empty session). - except: - return {} - - def _module_encode(session_dict): - "Returns the given session dictionary pickled and encoded as a string." - from django.conf.settings import SECRET_KEY - pickled = pickle.dumps(session_dict) - pickled_md5 = md5.new(pickled + SECRET_KEY).hexdigest() - return base64.encodestring(pickled + pickled_md5) - - def _module_get_new_session_key(): - "Returns session key that isn't being used." - from django.conf.settings import SECRET_KEY - # The random module is seeded when this Apache child is created. - # Use person_id and SECRET_KEY as added salt. - while 1: - session_key = md5.new(str(random.randint(0, sys.maxint - 1)) + str(random.randint(0, sys.maxint - 1)) + SECRET_KEY).hexdigest() - try: - get_object(session_key__exact=session_key) - except SessionDoesNotExist: - break - return session_key - - def _module_save(session_key, session_dict, expire_date): - s = Session(session_key, encode(session_dict), expire_date) - if session_dict: - s.save() - else: - s.delete() # Clear sessions with no data. - return s diff --git a/django/parts/auth/anonymoususers.py b/django/parts/auth/anonymoususers.py deleted file mode 100644 index fea93e2f5a..0000000000 --- a/django/parts/auth/anonymoususers.py +++ /dev/null @@ -1,44 +0,0 @@ -class AnonymousUser: - id = None - - def __init__(self): - pass - - def __repr__(self): - return 'AnonymousUser' - - def save(self): - raise NotImplementedError - - def delete(self): - raise NotImplementedError - - def set_password(self, raw_password): - raise NotImplementedError - - def check_password(self, raw_password): - raise NotImplementedError - - def get_group_list(self): - return [] - - def set_groups(self, group_id_list): - raise NotImplementedError - - def get_permission_list(self): - return [] - - def set_permissions(self, permission_id_list): - raise NotImplementedError - - def has_perm(self, perm): - return False - - def has_module_perms(self, module): - return False - - def get_and_delete_messages(self): - return [] - - def is_anonymous(self): - return True diff --git a/django/parts/auth/formfields.py b/django/parts/auth/formfields.py deleted file mode 100644 index cfbad248da..0000000000 --- a/django/parts/auth/formfields.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.models.auth import users -from django.core import formfields, validators - -class AuthenticationForm(formfields.Manipulator): - """ - Base class for authenticating users. Extend this to get a form that accepts - username/password logins. - """ - def __init__(self, request=None): - """ - If request is passed in, the manipulator will validate that cookies are - enabled. Note that the request (a HttpRequest object) must have set a - cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before - running this validator. - """ - self.request = request - self.fields = [ - formfields.TextField(field_name="username", length=15, maxlength=30, is_required=True, - validator_list=[self.isValidUser, self.hasCookiesEnabled]), - formfields.PasswordField(field_name="password", length=15, maxlength=30, is_required=True, - validator_list=[self.isValidPasswordForUser]), - ] - self.user_cache = None - - def hasCookiesEnabled(self, field_data, all_data): - if self.request and not self.request.session.test_cookie_worked(): - raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.") - - def isValidUser(self, field_data, all_data): - try: - self.user_cache = users.get_object(username__exact=field_data) - except users.UserDoesNotExist: - raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") - - def isValidPasswordForUser(self, field_data, all_data): - if self.user_cache is not None and not self.user_cache.check_password(field_data): - self.user_cache = None - raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.") - - def get_user_id(self): - if self.user_cache: - return self.user_cache.id - return None - - def get_user(self): - return self.user_cache diff --git a/django/parts/media/photos.py b/django/parts/media/photos.py deleted file mode 100644 index a14b3de19b..0000000000 --- a/django/parts/media/photos.py +++ /dev/null @@ -1,6 +0,0 @@ -import re - -def get_thumbnail_url(photo_url, width): - bits = photo_url.split('/') - bits[-1] = re.sub(r'(?i)\.(gif|jpg)$', '_t%s.\\1' % width, bits[-1]) - return '/'.join(bits) diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py new file mode 100644 index 0000000000..b42ede0339 --- /dev/null +++ b/django/shortcuts/__init__.py @@ -0,0 +1,23 @@ +# This module collects helper functions and classes that "span" multiple levels +# of MVC. In other words, these functions/classes introduce controlled coupling +# for convenience's sake. + +from django.template import loader +from django.http import HttpResponse, Http404 + + +def render_to_response(*args, **kwargs): + return HttpResponse(loader.render_to_string(*args, **kwargs)) +load_and_render = render_to_response # For backwards compatibility. + +def get_object_or_404(klass, **kwargs): + try: + return klass._default_manager.get(**kwargs) + except klass.DoesNotExist: + raise Http404 + +def get_list_or_404(klass, **kwargs): + obj_list = list(klass._default_manager.filter(**kwargs)) + if not obj_list: + raise Http404 + return obj_list diff --git a/django/core/template/__init__.py b/django/template/__init__.py index fe1a9cb338..e32200cccb 100644 --- a/django/core/template/__init__.py +++ b/django/template/__init__.py @@ -57,9 +57,10 @@ times with multiple contexts) import re from inspect import getargspec from django.utils.functional import curry -from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG, TEMPLATE_STRING_IF_INVALID +from django.conf import settings +from django.template.context import Context, RequestContext, ContextPopException -__all__ = ('Template','Context','compile_string') +__all__ = ('Template', 'Context', 'RequestContext', 'compile_string') TOKEN_TEXT = 0 TOKEN_VAR = 1 @@ -92,20 +93,12 @@ builtins = [] class TemplateSyntaxError(Exception): pass -class ContextPopException(Exception): - "pop() has been called more times than push()" - pass - class TemplateDoesNotExist(Exception): pass class VariableDoesNotExist(Exception): pass -class SilentVariableFailure(Exception): - "Any function raising this exception will be ignored by resolve_variable" - pass - class InvalidTemplateLibrary(Exception): pass @@ -130,7 +123,7 @@ class StringOrigin(Origin): class Template: def __init__(self, template_string, origin=None): "Compilation stage" - if TEMPLATE_DEBUG and origin == None: + if settings.TEMPLATE_DEBUG and origin == None: origin = StringOrigin(template_string) # Could do some crazy stack-frame stuff to record where this string # came from... @@ -151,58 +144,6 @@ def compile_string(template_string, origin): parser = parser_factory(lexer.tokenize()) return parser.parse() -class Context: - "A stack container for variable context" - def __init__(self, dict=None): - dict = dict or {} - self.dicts = [dict] - - def __repr__(self): - return repr(self.dicts) - - def __iter__(self): - for d in self.dicts: - yield d - - def push(self): - self.dicts = [{}] + self.dicts - - def pop(self): - if len(self.dicts) == 1: - raise ContextPopException - del self.dicts[0] - - def __setitem__(self, key, value): - "Set a variable in the current context" - self.dicts[0][key] = value - - def __getitem__(self, key): - "Get a variable's value, starting at the current context and going upward" - for dict in self.dicts: - if dict.has_key(key): - return dict[key] - return TEMPLATE_STRING_IF_INVALID - - def __delitem__(self, key): - "Delete a variable from the current context" - del self.dicts[0][key] - - def has_key(self, key): - for dict in self.dicts: - if dict.has_key(key): - return True - return False - - def get(self, key, otherwise): - for dict in self.dicts: - if dict.has_key(key): - return dict[key] - return otherwise - - def update(self, other_dict): - "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." - self.dicts = [other_dict] + self.dicts - class Token: def __init__(self, token_type, contents): "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK" @@ -257,7 +198,7 @@ class DebugLexer(Lexer): upto = end last_bit = self.template_string[upto:] if last_bit: - token_tups.append( (last_bit, (upto, upto + len(last_bit))) ) + token_tups.append( (last_bit, (upto, upto + len(last_bit))) ) return [self.create_token(tok, (self.origin, loc)) for tok, loc in token_tups] def create_token(self, token_string, source): @@ -304,7 +245,7 @@ class Parser(object): compiled_result = compile_func(self, token) except TemplateSyntaxError, e: if not self.compile_function_error(token, e): - raise + raise self.extend_nodelist(nodelist, compiled_result, token) self.exit_command() if parse_until: @@ -412,12 +353,19 @@ class DebugParser(Parser): if not hasattr(e, 'source'): e.source = token.source -if TEMPLATE_DEBUG: - lexer_factory = DebugLexer - parser_factory = DebugParser -else: - lexer_factory = Lexer - parser_factory = Parser + +def lexer_factory(*args, **kwargs): + if settings.TEMPLATE_DEBUG: + return DebugLexer(*args, **kwargs) + else: + return Lexer(*args, **kwargs) + +def parser_factory(*args, **kwargs): + if settings.TEMPLATE_DEBUG: + return DebugParser(*args, **kwargs) + else: + return Parser(*args, **kwargs) + class TokenParser: """ @@ -571,9 +519,9 @@ class FilterExpression(object): args = [] constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg") if i18n_arg: - args.append((False, _(i18n_arg.replace('\\', '')))) + args.append((False, _(i18n_arg.replace(r'\"', '"')))) elif constant_arg: - args.append((False, constant_arg.replace('\\', ''))) + args.append((False, constant_arg.replace(r'\"', '"'))) elif var_arg: args.append((True, var_arg)) filter_func = parser.find_filter(filter_name) @@ -588,7 +536,7 @@ class FilterExpression(object): try: obj = resolve_variable(self.var, context) except VariableDoesNotExist: - obj = TEMPLATE_STRING_IF_INVALID + obj = settings.TEMPLATE_STRING_IF_INVALID for func, args in self.filters: arg_vals = [] for lookup, arg in args: @@ -636,7 +584,7 @@ def resolve_variable(path, context): """ Returns the resolved variable, which may contain attribute syntax, within the given context. The variable may be a hard-coded string (if it begins - and ends with single or double quote marks), or an integer or float literal. + and ends with single or double quote marks). >>> c = {'article': {'section':'News'}} >>> resolve_variable('article.section', c) @@ -655,9 +603,9 @@ def resolve_variable(path, context): if path[0] in '0123456789': number_type = '.' in path and float or int try: - current = number_type(path) + current = number_type(path) except ValueError: - current = TEMPLATE_STRING_IF_INVALID + current = settings.TEMPLATE_STRING_IF_INVALID elif path[0] in ('"', "'") and path[0] == path[-1]: current = path[1:-1] else: @@ -671,21 +619,29 @@ def resolve_variable(path, context): current = getattr(current, bits[0]) if callable(current): if getattr(current, 'alters_data', False): - current = TEMPLATE_STRING_IF_INVALID + current = settings.TEMPLATE_STRING_IF_INVALID else: try: # method call (assuming no args required) current = current() - except SilentVariableFailure: - current = TEMPLATE_STRING_IF_INVALID except TypeError: # arguments *were* required # GOTCHA: This will also catch any TypeError # raised in the function itself. - current = TEMPLATE_STRING_IF_INVALID # invalid method call + current = settings.TEMPLATE_STRING_IF_INVALID # invalid method call + except Exception, e: + if getattr(e, 'silent_variable_failure', False): + current = settings.TEMPLATE_STRING_IF_INVALID + else: + raise except (TypeError, AttributeError): try: # list-index lookup current = current[int(bits[0])] except (IndexError, ValueError, KeyError): raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute + except Exception, e: + if getattr(e, 'silent_variable_failure', False): + current = settings.TEMPLATE_STRING_IF_INVALID + else: + raise del bits[0] return current @@ -764,7 +720,7 @@ class VariableNode(Node): if not isinstance(output, basestring): return str(output) elif isinstance(output, unicode): - return output.encode(DEFAULT_CHARSET) + return output.encode(settings.DEFAULT_CHARSET) else: return output @@ -775,7 +731,7 @@ class VariableNode(Node): class DebugVariableNode(VariableNode): def render(self, context): try: - output = self.filter_expression.resolve(context) + output = self.filter_expression.resolve(context) except TemplateSyntaxError, e: if not hasattr(e, 'source'): e.source = self.source @@ -888,7 +844,7 @@ class Library(object): dict = func(*args) if not getattr(self, 'nodelist', False): - from django.core.template_loader import get_template + from django.template.loader import get_template t = get_template(file_name) self.nodelist = t.nodelist return self.nodelist.render(context_class(dict)) @@ -916,5 +872,5 @@ def get_library(module_name): def add_to_builtins(module_name): builtins.append(get_library(module_name)) -add_to_builtins('django.core.template.defaulttags') -add_to_builtins('django.core.template.defaultfilters') +add_to_builtins('django.template.defaulttags') +add_to_builtins('django.template.defaultfilters') diff --git a/django/template/context.py b/django/template/context.py new file mode 100644 index 0000000000..f50fb07598 --- /dev/null +++ b/django/template/context.py @@ -0,0 +1,97 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +_standard_context_processors = None + +class ContextPopException(Exception): + "pop() has been called more times than push()" + pass + +class Context: + "A stack container for variable context" + def __init__(self, dict_=None): + dict_ = dict_ or {} + self.dicts = [dict_] + + def __repr__(self): + return repr(self.dicts) + + def __iter__(self): + for d in self.dicts: + yield d + + def push(self): + self.dicts = [{}] + self.dicts + + def pop(self): + if len(self.dicts) == 1: + raise ContextPopException + del self.dicts[0] + + def __setitem__(self, key, value): + "Set a variable in the current context" + self.dicts[0][key] = value + + def __getitem__(self, key): + "Get a variable's value, starting at the current context and going upward" + for d in self.dicts: + if d.has_key(key): + return d[key] + return settings.TEMPLATE_STRING_IF_INVALID + + def __delitem__(self, key): + "Delete a variable from the current context" + del self.dicts[0][key] + + def has_key(self, key): + for d in self.dicts: + if d.has_key(key): + return True + return False + + def get(self, key, otherwise): + for d in self.dicts: + if d.has_key(key): + return d[key] + return otherwise + + def update(self, other_dict): + "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." + self.dicts = [other_dict] + self.dicts + +# This is a function rather than module-level procedural code because we only +# want it to execute if somebody uses RequestContext. +def get_standard_processors(): + global _standard_context_processors + if _standard_context_processors is None: + processors = [] + for path in settings.TEMPLATE_CONTEXT_PROCESSORS: + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = __import__(module, '', '', [attr]) + except ImportError, e: + raise ImproperlyConfigured, 'Error importing request processor module %s: "%s"' % (module, e) + try: + func = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable request processor' % (module, attr) + processors.append(func) + _standard_context_processors = tuple(processors) + return _standard_context_processors + +class RequestContext(Context): + """ + This subclass of template.Context automatically populates itself using + the processors defined in TEMPLATE_CONTEXT_PROCESSORS. + Additional processors can be specified as a list of callables + using the "processors" keyword argument. + """ + def __init__(self, request, dict=None, processors=None): + Context.__init__(self, dict) + if processors is None: + processors = () + else: + processors = tuple(processors) + for processor in get_standard_processors() + processors: + self.update(processor(request)) diff --git a/django/core/template/defaultfilters.py b/django/template/defaultfilters.py index b82d54a31c..fa4975e643 100644 --- a/django/core/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -1,7 +1,7 @@ "Default variable filters" -from django.core.template import resolve_variable, Library -from django.conf.settings import DATE_FORMAT, TIME_FORMAT +from django.template import resolve_variable, Library +from django.conf import settings from django.utils.translation import gettext import re import random as random_module @@ -133,7 +133,7 @@ def wordwrap(value, arg): """ Wraps words at specified line length - Argument: number of characters at which to wrap the text + Argument: number of words to wrap the text at. """ from django.utils.text import wrap return wrap(str(value), int(arg)) @@ -327,12 +327,12 @@ def get_digit(value, arg): # DATES # ################### -def date(value, arg=DATE_FORMAT): +def date(value, arg=settings.DATE_FORMAT): "Formats a date according to the given format" from django.utils.dateformat import format return format(value, arg) -def time(value, arg=TIME_FORMAT): +def time(value, arg=settings.TIME_FORMAT): "Formats a time according to the given format" from django.utils.dateformat import time_format return time_format(value, arg) @@ -431,8 +431,11 @@ def phone2numeric(value): def pprint(value): "A wrapper around pprint.pprint -- for debugging, really" from pprint import pformat - return pformat(value) - + try: + return pformat(value) + except Exception, e: + return "Error in formatting:%s" % e + # Syntax: register.filter(name of filter, callback) register.filter(add) register.filter(addslashes) diff --git a/django/core/template/defaulttags.py b/django/template/defaulttags.py index 1438b9f74a..18f1b9ab30 100644 --- a/django/core/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -1,8 +1,9 @@ "Default tags used by the template system, available to all templates." -from django.core.template import Node, NodeList, Template, Context, resolve_variable -from django.core.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END -from django.core.template import get_library, Library, InvalidTemplateLibrary +from django.template import Node, NodeList, Template, Context, resolve_variable +from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END +from django.template import get_library, Library, InvalidTemplateLibrary +from django.conf import settings import sys register = Library() @@ -121,7 +122,12 @@ class IfChangedNode(Node): def render(self, context): content = self.nodelist.render(context) if content != self._last_seen: + firstloop = (self._last_seen == None) self._last_seen = content + context.push() + context['ifchanged'] = {'firstloop': firstloop} + content = self.nodelist.render(context) + context.pop() return content else: return '' @@ -196,8 +202,7 @@ class RegroupNode(Node): return '' def include_is_allowed(filepath): - from django.conf.settings import ALLOWED_INCLUDE_ROOTS - for root in ALLOWED_INCLUDE_ROOTS: + for root in settings.ALLOWED_INCLUDE_ROOTS: if filepath.startswith(root): return True return False @@ -207,9 +212,8 @@ class SsiNode(Node): self.filepath, self.parsed = filepath, parsed def render(self, context): - from django.conf.settings import DEBUG if not include_is_allowed(self.filepath): - if DEBUG: + if settings.DEBUG: return "[Didn't have permission to include file]" else: return '' # Fail silently for invalid includes. @@ -224,7 +228,7 @@ class SsiNode(Node): t = Template(output) return t.render(context) except TemplateSyntaxError, e: - if DEBUG: + if settings.DEBUG: return "[Included template had syntax error: %s]" % e else: return '' # Fail silently for invalid included templates. @@ -340,6 +344,8 @@ def cycle(parser, token): elif len(args) == 2: name = args[1] + if not hasattr(parser, '_namedCycleNodes'): + raise TemplateSyntaxError("No named cycles in template: '%s' is not defined" % name) if not parser._namedCycleNodes.has_key(name): raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) return parser._namedCycleNodes[name] diff --git a/django/core/template/loader.py b/django/template/loader.py index 3d83a1494f..ebca582ef9 100644 --- a/django/core/template/loader.py +++ b/django/template/loader.py @@ -21,8 +21,8 @@ # installed, because pkg_resources is necessary to read eggs. from django.core.exceptions import ImproperlyConfigured -from django.core.template import Origin, StringOrigin, Template, Context, TemplateDoesNotExist, add_to_builtins -from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG +from django.template import Origin, StringOrigin, Template, Context, TemplateDoesNotExist, add_to_builtins +from django.conf import settings template_source_loaders = None @@ -35,7 +35,7 @@ class LoaderOrigin(Origin): return self.loader(self.loadname, self.dirs)[0] def make_origin(display_name, loader, name, dirs): - if TEMPLATE_DEBUG: + if settings.TEMPLATE_DEBUG: return LoaderOrigin(display_name, loader, name, dirs) else: return None @@ -47,7 +47,7 @@ def find_template_source(name, dirs=None): global template_source_loaders if template_source_loaders is None: template_source_loaders = [] - for path in TEMPLATE_LOADERS: + for path in settings.TEMPLATE_LOADERS: i = path.rfind('.') module, attr = path[:i], path[i+1:] try: @@ -113,4 +113,4 @@ def select_template(template_name_list): # If we get here, none of the templates could be loaded raise TemplateDoesNotExist, ', '.join(template_name_list) -add_to_builtins('django.core.template.loader_tags') +add_to_builtins('django.template.loader_tags') diff --git a/django/core/template/loader_tags.py b/django/template/loader_tags.py index 238ece348c..e0d9a70a98 100644 --- a/django/core/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -1,7 +1,8 @@ -from django.core.template import TemplateSyntaxError, TemplateDoesNotExist, resolve_variable -from django.core.template import Library, Context, Node -from django.core.template.loader import get_template, get_template_from_string, find_template_source -from django.conf.settings import TEMPLATE_DEBUG +from django.template import TemplateSyntaxError, TemplateDoesNotExist, resolve_variable +from django.template import Library, Context, Node +from django.template.loader import get_template, get_template_from_string, find_template_source +from django.conf import settings + register = Library() class ExtendsError(Exception): @@ -84,8 +85,8 @@ class ConstantIncludeNode(Node): t = get_template(template_path) self.template = t except: - if TEMPLATE_DEBUG: - raise + if settings.TEMPLATE_DEBUG: + raise self.template = None def render(self, context): @@ -99,16 +100,16 @@ class IncludeNode(Node): self.template_name = template_name def render(self, context): - try: - template_name = resolve_variable(self.template_name, context) - t = get_template(template_name) - return t.render(context) - except TemplateSyntaxError, e: - if TEMPLATE_DEBUG: + try: + template_name = resolve_variable(self.template_name, context) + t = get_template(template_name) + return t.render(context) + except TemplateSyntaxError, e: + if settings.TEMPLATE_DEBUG: raise - return '' - except: - return '' # Fail silently for invalid included templates. + return '' + except: + return '' # Fail silently for invalid included templates. def do_block(parser, token): """ diff --git a/django/template/loaders/__init__.py b/django/template/loaders/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/template/loaders/__init__.py diff --git a/django/core/template/loaders/app_directories.py b/django/template/loaders/app_directories.py index 390e47852e..8a9bfef4b6 100644 --- a/django/core/template/loaders/app_directories.py +++ b/django/template/loaders/app_directories.py @@ -1,13 +1,13 @@ # Wrapper for loading templates from "template" directories in installed app packages. -from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION +from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.core.template import TemplateDoesNotExist +from django.template import TemplateDoesNotExist import os # At compile time, cache the directories to search. app_template_dirs = [] -for app in INSTALLED_APPS: +for app in settings.INSTALLED_APPS: i = app.rfind('.') if i == -1: m, a = app, None @@ -29,7 +29,7 @@ app_template_dirs = tuple(app_template_dirs) def get_template_sources(template_name, template_dirs=None): for template_dir in app_template_dirs: - yield os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION + yield os.path.join(template_dir, template_name) def load_template_source(template_name, template_dirs=None): for filepath in get_template_sources(template_name, template_dirs): diff --git a/django/core/template/loaders/eggs.py b/django/template/loaders/eggs.py index 5d48326dce..6184aeaccf 100644 --- a/django/core/template/loaders/eggs.py +++ b/django/template/loaders/eggs.py @@ -5,8 +5,8 @@ try: except ImportError: resource_string = None -from django.core.template import TemplateDoesNotExist -from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION +from django.template import TemplateDoesNotExist +from django.conf import settings def load_template_source(template_name, template_dirs=None): """ @@ -15,8 +15,8 @@ def load_template_source(template_name, template_dirs=None): For every installed app, it tries to get the resource (app, template_name). """ if resource_string is not None: - pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION - for app in INSTALLED_APPS: + pkg_name = 'templates/' + template_name + for app in settings.INSTALLED_APPS: try: return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name)) except: diff --git a/django/core/template/loaders/filesystem.py b/django/template/loaders/filesystem.py index 23ce6cd9e4..0c94021fb8 100644 --- a/django/core/template/loaders/filesystem.py +++ b/django/template/loaders/filesystem.py @@ -1,14 +1,14 @@ # Wrapper for loading templates from the filesystem. -from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION -from django.core.template import TemplateDoesNotExist +from django.conf import settings +from django.template import TemplateDoesNotExist import os def get_template_sources(template_name, template_dirs=None): if not template_dirs: - template_dirs = TEMPLATE_DIRS + template_dirs = settings.TEMPLATE_DIRS for template_dir in template_dirs: - yield os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION + yield os.path.join(template_dir, template_name) def load_template_source(template_name, template_dirs=None): tried = [] diff --git a/django/templatetags/__init__.py b/django/templatetags/__init__.py index 538da5b354..62374577ea 100644 --- a/django/templatetags/__init__.py +++ b/django/templatetags/__init__.py @@ -1,6 +1,6 @@ -from django.conf.settings import INSTALLED_APPS +from django.conf import settings -for a in INSTALLED_APPS: +for a in settings.INSTALLED_APPS: try: __path__.extend(__import__(a + '.templatetags', '', '', ['']).__path__) except ImportError: diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 7c2019cac0..0c601535af 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -1,6 +1,6 @@ -from django.core.template import Node, NodeList, Template, Context, resolve_variable -from django.core.template import TemplateSyntaxError, TokenParser, Library -from django.core.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR +from django.template import Node, NodeList, Template, Context, resolve_variable +from django.template import TemplateSyntaxError, TokenParser, Library +from django.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR from django.utils import translation import re, sys @@ -11,8 +11,8 @@ class GetAvailableLanguagesNode(Node): self.variable = variable def render(self, context): - from django.conf.settings import LANGUAGES - context[self.variable] = LANGUAGES + from django.conf import settings + context[self.variable] = settings.LANGUAGES return '' class GetCurrentLanguageNode(Node): diff --git a/django/utils/cache.py b/django/utils/cache.py index b888cef179..5eba302ebe 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -80,8 +80,17 @@ def patch_response_headers(response, cache_timeout=None): if not response.has_header('Expires'): expires = now + datetime.timedelta(0, cache_timeout) response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT') + if cache_timeout < 0: + cache_timeout = 0 # Can't have max-age negative patch_cache_control(response, max_age=cache_timeout) +def add_never_cache_headers(response): + """ + Add headers to a response to indicate that + a page should never be cached. + """ + patch_response_headers(response, cache_timeout=-1) + def patch_vary_headers(response, newheaders): """ Adds (or updates) the "Vary" header in the given HttpResponse object. diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 20aa30bcff..bc8fb07ef5 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -40,6 +40,43 @@ class MergeDict: return True return False +class SortedDict(dict): + "A dictionary that keeps its keys in the order in which they're inserted." + def __init__(self, data={}): + dict.__init__(self, data) + self.keyOrder = data.keys() + + def __setitem__(self, key, value): + dict.__setitem__(self, key, value) + if key not in self.keyOrder: + self.keyOrder.append(key) + + def __delitem__(self, key): + dict.__delitem__(self, key) + self.keyOrder.remove(key) + + def __iter__(self): + for k in self.keyOrder: + yield k + + def items(self): + return zip(self.keyOrder, self.values()) + + def keys(self): + return self.keyOrder[:] + + def values(self): + return [dict.__getitem__(self,k) for k in self.keyOrder] + + def update(self, dict): + for k, v in dict.items(): + self.__setitem__(k, v) + + def setdefault(self, key, default): + if key not in self.keyOrder: + self.keyOrder.append(key) + return dict.setdefault(self, key, default) + class MultiValueDictKeyError(KeyError): pass @@ -193,4 +230,4 @@ class DotExpandedDict(dict): try: current[bits[-1]] = v except TypeError: # Special-case if current isn't a dict. - current = {bits[-1]: v} + current = {bits[-1] : v} diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index 22db5abe7f..f7c25f2933 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -21,8 +21,6 @@ http://diveintomark.org/archives/2004/02/04/incompatible-rss from django.utils.xmlutils import SimplerXMLGenerator import datetime, re, time import email.Utils -from xml.dom import minidom -from xml.parsers.expat import ExpatError def rfc2822_date(date): return email.Utils.formatdate(time.mktime(date.timetuple())) @@ -158,9 +156,11 @@ class Rss201rev2Feed(RssFeed): handler.addQuickElement(u"description", item['description']) # Author information. - if item['author_email'] is not None and item['author_name'] is not None: - handler.addQuickElement(u"author", u"%s (%s)" % \ + if item["author_name"] and item["author_email"]: + handler.addQuickElement(u"author", "%s (%s)" % \ (item['author_email'], item['author_name'])) + elif item["author_email"]: + handler.addQuickElement(u"author", item["author_email"]) if item['pubdate'] is not None: handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii')) diff --git a/django/utils/functional.py b/django/utils/functional.py index 69aeb81850..d1514d5728 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -24,14 +24,14 @@ def lazy(func, *resultclasses): # the evaluation and store the result. Afterwards, the result # is delivered directly. So the result is memoized. def __init__(self, args, kw): - self.__func = func - self.__args = args - self.__kw = kw - self.__dispatch = {} - for resultclass in resultclasses: - self.__dispatch[resultclass] = {} - for (k, v) in resultclass.__dict__.items(): - setattr(self, k, self.__promise__(resultclass, k, v)) + self.__func = func + self.__args = args + self.__kw = kw + self.__dispatch = {} + for resultclass in resultclasses: + self.__dispatch[resultclass] = {} + for (k, v) in resultclass.__dict__.items(): + setattr(self, k, self.__promise__(resultclass, k, v)) def __promise__(self, klass, funcname, func): # Builds a wrapper around some magic method and registers that magic diff --git a/django/utils/html.py b/django/utils/html.py index 6c9779a156..a0d1e82dcf 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -25,7 +25,7 @@ def escape(html): "Returns the given HTML with ampersands, quotes and carets encoded" if not isinstance(html, basestring): html = str(html) - return html.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') + return html.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') def linebreaks(value): "Converts newlines into <p> and <br />s" diff --git a/django/utils/termcolors.py b/django/utils/termcolors.py new file mode 100644 index 0000000000..3ce1d5bb6b --- /dev/null +++ b/django/utils/termcolors.py @@ -0,0 +1,70 @@ +""" +termcolors.py +""" + +import types + +color_names = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') +foreground = dict([(color_names[x], '3%s' % x) for x in range(8)]) +background = dict([(color_names[x], '4%s' % x) for x in range(8)]) +del color_names + +RESET = '0' +opt_dict = {'bold': '1', 'underscore': '4', 'blink': '5', 'reverse': '7', 'conceal': '8'} + +def colorize(text='', opts=(), **kwargs): + """ + Returns your text, enclosed in ANSI graphics codes. + + Depends on the keyword arguments 'fg' and 'bg', and the contents of + the opts tuple/list. + + Returns the RESET code if no parameters are given. + + Valid colors: + 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + + Valid options: + 'bold' + 'underscore' + 'blink' + 'reverse' + 'conceal' + 'noreset' - string will not be auto-terminated with the RESET code + + Examples: + colorize('hello', fg='red', bg='blue', opts=('blink',)) + colorize() + colorize('goodbye', opts=('underscore',)) + print colorize('first line', fg='red', opts=('noreset',)) + print 'this should be red too' + print colorize('and so should this') + print 'this should not be red' + """ + text = str(text) + code_list = [] + if text == '' and len(opts) == 1 and opts[0] == 'reset': + return '\x1b[%sm' % RESET + for k, v in kwargs.iteritems(): + if k == 'fg': + code_list.append(foreground[v]) + elif k == 'bg': + code_list.append(background[v]) + for o in opts: + if o in opt_dict: + code_list.append(opt_dict[o]) + if 'noreset' not in opts: + text = text + '\x1b[%sm' % RESET + return ('\x1b[%sm' % ';'.join(code_list)) + text + +def make_style(opts=(), **kwargs): + """ + Returns a function with default parameters for colorize() + + Example: + bold_red = make_style(opts=('bold',), fg='red') + print bold_red('hello') + KEYWORD = make_style(fg='yellow') + COMMENT = make_style(fg='blue', opts=('bold',)) + """ + return lambda text: colorize(text, opts, **kwargs) diff --git a/django/utils/text.py b/django/utils/text.py index ce1225b83c..7b6e1182ab 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -1,6 +1,6 @@ import re -from django.conf.settings import DEFAULT_CHARSET +from django.conf import settings # Capitalizes the first letter of a string. capfirst = lambda x: x and x[0].upper() + x[1:] @@ -100,7 +100,7 @@ def javascript_quote(s): return r"\u%04x" % ord(match.group(1)) if type(s) == str: - s = s.decode(DEFAULT_CHARSET) + s = s.decode(settings.DEFAULT_CHARSET) elif type(s) != unicode: raise TypeError, s s = s.replace('\\', '\\\\') diff --git a/django/utils/timesince.py b/django/utils/timesince.py index a16616584b..bc4f969dc4 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -11,6 +11,7 @@ def timesince(d, now=None): chunks = ( (60 * 60 * 24 * 365, lambda n: ngettext('year', 'years', n)), (60 * 60 * 24 * 30, lambda n: ngettext('month', 'months', n)), + (60 * 60 * 24 * 7, lambda n : ngettext('week', 'weeks', n)), (60 * 60 * 24, lambda n : ngettext('day', 'days', n)), (60 * 60, lambda n: ngettext('hour', 'hours', n)), (60, lambda n: ngettext('minute', 'minutes', n)) diff --git a/django/utils/translation.py b/django/utils/translation.py index 56cd5426f0..a877f60009 100644 --- a/django/utils/translation.py +++ b/django/utils/translation.py @@ -115,7 +115,7 @@ def translation(language): if sys.version_info < (2, 4): klass = DjangoTranslation23 - globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale') + globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale') parts = settings.SETTINGS_MODULE.split('.') project = __import__(parts[0], {}, {}, []) @@ -209,8 +209,8 @@ def get_language(): except AttributeError: pass # If we don't have a real translation object, assume it's the default language. - from django.conf.settings import LANGUAGE_CODE - return LANGUAGE_CODE + from django.conf import settings + return settings.LANGUAGE_CODE def catalog(): """ @@ -275,7 +275,7 @@ def check_for_language(lang_code): only used for language codes from either the cookies or session. """ from django.conf import settings - globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale') + globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale') if gettext_module.find('django', globalpath, [to_locale(lang_code)]) is not None: return True else: @@ -289,7 +289,7 @@ def get_language_from_request(request): """ global _accepted from django.conf import settings - globalpath = os.path.join(os.path.dirname(settings.__file__), 'locale') + globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale') supported = dict(settings.LANGUAGES) if hasattr(request, 'session'): @@ -346,16 +346,16 @@ def get_date_formats(): technical message ID to store date and time formats. If it doesn't contain one, the formats provided in the settings will be used. """ - from django.conf.settings import DATE_FORMAT, DATETIME_FORMAT, TIME_FORMAT + from django.conf import settings date_format = _('DATE_FORMAT') datetime_format = _('DATETIME_FORMAT') time_format = _('TIME_FORMAT') if date_format == 'DATE_FORMAT': - date_format = DATE_FORMAT + date_format = settings.DATE_FORMAT if datetime_format == 'DATETIME_FORMAT': - datetime_format = DATETIME_FORMAT + datetime_format = settings.DATETIME_FORMAT if time_format == 'TIME_FORMAT': - time_format = TIME_FORMAT + time_format = settings.TIME_FORMAT return (date_format, datetime_format, time_format) def install(): @@ -384,7 +384,7 @@ def templatize(src): does so by translating the Django translation tags into standard gettext function invocations. """ - from django.core.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK + from django.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK out = StringIO() intrans = False inplural = False @@ -457,3 +457,13 @@ def templatize(src): else: out.write(blankout(t.contents, 'X')) return out.getvalue() + +def string_concat(*strings): + """" + lazy variant of string concatenation, needed for translations that are + constructed from multiple parts. Handles lazy strings and non-strings by + first turning all arguments to strings, before joining them. + """ + return ''.join([str(el) for el in strings]) + +string_concat = lazy(string_concat, str) diff --git a/django/views/auth/login.py b/django/views/auth/login.py deleted file mode 100644 index 3f2bd43015..0000000000 --- a/django/views/auth/login.py +++ /dev/null @@ -1,49 +0,0 @@ -from django.parts.auth.formfields import AuthenticationForm -from django.core import formfields -from django.core.extensions import DjangoContext, render_to_response -from django.models.auth import users -from django.models.core import sites -from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect - -REDIRECT_FIELD_NAME = 'next' -LOGIN_URL = '/accounts/login/' - -def login(request): - "Displays the login form and handles the login action." - manipulator = AuthenticationForm(request) - redirect_to = request.REQUEST.get(REDIRECT_FIELD_NAME, '') - if request.POST: - errors = manipulator.get_validation_errors(request.POST) - if not errors: - # Light security check -- make sure redirect_to isn't garbage. - if not redirect_to or '://' in redirect_to or ' ' in redirect_to: - redirect_to = '/accounts/profile/' - request.session[users.SESSION_KEY] = manipulator.get_user_id() - request.session.delete_test_cookie() - return HttpResponseRedirect(redirect_to) - else: - errors = {} - request.session.set_test_cookie() - return render_to_response('registration/login', { - 'form': formfields.FormWrapper(manipulator, request.POST, errors), - REDIRECT_FIELD_NAME: redirect_to, - 'site_name': sites.get_current().name, - }, context_instance=DjangoContext(request)) - -def logout(request, next_page=None): - "Logs out the user and displays 'You are logged out' message." - try: - del request.session[users.SESSION_KEY] - except KeyError: - return render_to_response('registration/logged_out', context_instance=DjangoContext(request)) - else: - # Redirect to this page until the session has been cleared. - return HttpResponseRedirect(next_page or request.path) - -def logout_then_login(request, login_url=LOGIN_URL): - "Logs out the user if he is logged in. Then redirects to the log-in page." - return logout(request, login_url) - -def redirect_to_login(next, login_url=LOGIN_URL): - "Redirects the user to the login page, passing the given 'next' page" - return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, next)) diff --git a/django/views/debug.py b/django/views/debug.py index b08a56a524..aa0a93b863 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -1,7 +1,7 @@ from django.conf import settings -from django.core.template import Template, Context, TemplateDoesNotExist +from django.template import Template, Context, TemplateDoesNotExist from django.utils.html import escape -from django.utils.httpwrappers import HttpResponseServerError, HttpResponseNotFound +from django.http import HttpResponseServerError, HttpResponseNotFound import os, re from itertools import count, izip from os.path import dirname, join as pathjoin @@ -72,7 +72,7 @@ def technical_500_response(request, exc_type, exc_value, tb): template_does_not_exist = False loader_debug_info = None if issubclass(exc_type, TemplateDoesNotExist): - from django.core.template.loader import template_source_loaders + from django.template.loader import template_source_loaders template_does_not_exist = True loader_debug_info = [] for loader in template_source_loaders: @@ -641,8 +641,8 @@ EMPTY_URLCONF_TEMPLATE = """ <div id="instructions"> <p>Of course, you haven't actually done any work yet. Here's what to do next:</p> <ul> - <li>Edit the <code>DATABASE_*</code> settings in <code>{{ project_name }}/settings.py</code>.</li> - <li>Start your first app by running <code>{{ project_name }}/manage.py startapp [appname]</code>.</li> + <li>If you plan to use a database, edit the <code>DATABASE_*</code> settings in <code>{{ project_name }}/settings.py</code>.</li> + <li>Start your first app by running <code>python {{ project_name }}/manage.py startapp [appname]</code>.</li> </ul> </div> diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index f86372cf4e..5467ff501e 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -13,7 +13,7 @@ account on caching -- just like the middleware does. import re from django.utils.decorators import decorator_from_middleware -from django.utils.cache import patch_cache_control +from django.utils.cache import patch_cache_control, add_never_cache_headers from django.middleware.cache import CacheMiddleware cache_page = decorator_from_middleware(CacheMiddleware) @@ -31,3 +31,13 @@ def cache_control(**kwargs): return _cache_controller +def never_cache(view_func): + """ + Decorator that adds headers to a response so that it will + never be cached. + """ + def _wrapped_view_func(request, *args, **kwargs): + response = view_func(request, *args, **kwargs) + add_never_cache_headers(response) + return response + return _wrapped_view_func diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index b9b6bac757..a15e82fcc7 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -4,7 +4,7 @@ Decorators for views based on HTTP headers. from django.utils.decorators import decorator_from_middleware from django.middleware.http import ConditionalGetMiddleware -from django.utils.httpwrappers import HttpResponseForbidden +from django.http import HttpResponseForbidden conditional_page = decorator_from_middleware(ConditionalGetMiddleware) diff --git a/django/views/defaults.py b/django/views/defaults.py index 95c18b4263..d5460a7495 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,69 +1,89 @@ -from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.template import Context, loader -from django.models.core import sites, contenttypes -from django.utils import httpwrappers +from django.core.exceptions import ObjectDoesNotExist +from django.template import Context, loader +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django import http def shortcut(request, content_type_id, object_id): "Redirect to an object's page based on a content-type ID and an object ID." # Look up the object, making sure it's got a get_absolute_url() function. try: - content_type = contenttypes.get_object(pk=content_type_id) + content_type = ContentType.objects.get(pk=content_type_id) obj = content_type.get_object_for_this_type(pk=object_id) except ObjectDoesNotExist: - raise Http404, "Content type %s object %s doesn't exist" % (content_type_id, object_id) + raise http.Http404, "Content type %s object %s doesn't exist" % (content_type_id, object_id) try: absurl = obj.get_absolute_url() except AttributeError: - raise Http404, "%s objects don't have get_absolute_url() methods" % content_type.name + raise http.Http404, "%s objects don't have get_absolute_url() methods" % content_type.name # Try to figure out the object's domain, so we can do a cross-site redirect # if necessary. # If the object actually defines a domain, we're done. if absurl.startswith('http://'): - return httpwrappers.HttpResponseRedirect(absurl) + return http.HttpResponseRedirect(absurl) object_domain = None - # Next, look for an many-to-many relationship to sites - if hasattr(obj, 'get_site_list'): - site_list = obj.get_site_list() - if site_list: - object_domain = site_list[0].domain + # Otherwise, we need to introspect the object's relationships for a + # relation to the Site object + opts = obj._meta - # Next, look for a many-to-one relationship to sites - elif hasattr(obj, 'get_site'): + # First, look for an many-to-many relationship to sites + for field in opts.many_to_many: + if field.rel.to is Site: + try: + object_domain = getattr(obj, field.name).all()[0].domain + except Site.DoesNotExist: + pass + if object_domain is not None: + break + + # Next look for a many-to-one relationship to site + if object_domain is None: + for field in obj._meta.fields: + if field.rel and field.rel.to is Site: + try: + object_domain = getattr(obj, field.name).domain + except Site.DoesNotExist: + pass + if object_domain is not None: + break + + # Fall back to the current site (if possible) + if object_domain is None: try: - object_domain = obj.get_site().domain - except sites.SiteDoesNotExist: + object_domain = Site.objects.get_current().domain + except Site.DoesNotExist: pass - # Then, fall back to the current site (if possible) + # If all that malarkey found an object domain, use it; otherwise fall back + # to whatever get_absolute_url() returned. + if object_domain is not None: + return http.HttpResponseRedirect('http://%s%s' % (object_domain, absurl)) else: - try: - object_domain = sites.get_current().domain - except sites.SiteDoesNotExist: - # Finally, give up and use a URL without the domain name - return httpwrappers.HttpResponseRedirect(obj.get_absolute_url()) - return httpwrappers.HttpResponseRedirect('http://%s%s' % (object_domain, obj.get_absolute_url())) + return http.HttpResponseRedirect(absurl) -def page_not_found(request, template_name='404'): +def page_not_found(request, template_name='404.html'): """ Default 404 handler, which looks for the requested URL in the redirects table, redirects if found, and displays 404 page if not redirected. - Templates: `404` - Context: None + Templates: `404.html` + Context: + request_path + The path of the requested URL (e.g., '/app/pages/bad_page/') """ t = loader.get_template(template_name) - return httpwrappers.HttpResponseNotFound(t.render(Context())) + return http.HttpResponseNotFound(t.render(Context({'request_path': request.path}))) -def server_error(request, template_name='500'): +def server_error(request, template_name='500.html'): """ 500 error handler. - Templates: `500` + Templates: `500.html` Context: None """ t = loader.get_template(template_name) - return httpwrappers.HttpResponseServerError(t.render(Context())) + return http.HttpResponseServerError(t.render(Context())) diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index e9b552df3b..0605744e3d 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -1,20 +1,20 @@ -from django import models from django.core.xheaders import populate_xheaders -from django.core.template import loader -from django.core import formfields, meta -from django.views.auth.login import redirect_to_login -from django.core.extensions import DjangoContext +from django.template import loader +from django import forms +from django.db.models import FileField +from django.contrib.auth.views import redirect_to_login +from django.template import RequestContext from django.core.paginator import ObjectPaginator, InvalidPage -from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect -from django.core.exceptions import Http404, ObjectDoesNotExist, ImproperlyConfigured +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -def create_object(request, app_label, module_name, template_name=None, +def create_object(request, model, template_name=None, template_loader=loader, extra_context={}, post_save_redirect=None, login_required=False, follow=None, context_processors=None): """ Generic object-creation function. - Templates: ``<app_label>/<module_name>_form`` + Templates: ``<app_label>/<model_name>_form.html`` Context: form the form wrapper for the object @@ -22,13 +22,12 @@ def create_object(request, app_label, module_name, template_name=None, if login_required and request.user.is_anonymous(): return redirect_to_login(request.path) - mod = models.get_module(app_label, module_name) - manipulator = mod.AddManipulator(follow=follow) + manipulator = model.AddManipulator(follow=follow) if request.POST: # If data was POSTed, we're trying to create a new object new_data = request.POST.copy() - if mod.Klass._meta.has_field_type(meta.FileField): + if model._meta.has_field_type(FileField): new_data.update(request.FILES) # Check for errors @@ -40,7 +39,7 @@ def create_object(request, app_label, module_name, template_name=None, new_object = manipulator.save(new_data) if not request.user.is_anonymous(): - request.user.add_message("The %s was created successfully." % mod.Klass._meta.verbose_name) + request.user.message_set.create(message="The %s was created successfully." % model._meta.verbose_name) # Redirect to the new object: first by trying post_save_redirect, # then by obj.get_absolute_url; fail if neither works. @@ -56,11 +55,11 @@ def create_object(request, app_label, module_name, template_name=None, new_data = manipulator.flatten_data() # Create the FormWrapper, template, context, response - form = formfields.FormWrapper(manipulator, new_data, errors) + form = forms.FormWrapper(manipulator, new_data, errors) if not template_name: - template_name = "%s/%s_form" % (app_label, module_name) + template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { 'form': form, }, context_processors) for key, value in extra_context.items(): @@ -70,15 +69,15 @@ def create_object(request, app_label, module_name, template_name=None, c[key] = value return HttpResponse(t.render(c)) -def update_object(request, app_label, module_name, object_id=None, slug=None, +def update_object(request, model, object_id=None, slug=None, slug_field=None, template_name=None, template_loader=loader, - extra_lookup_kwargs={}, extra_context={}, post_save_redirect=None, + extra_context={}, post_save_redirect=None, login_required=False, follow=None, context_processors=None, template_object_name='object'): """ Generic object-update function. - Templates: ``<app_label>/<module_name>_form`` + Templates: ``<app_label>/<model_name>_form.html`` Context: form the form wrapper for the object @@ -88,23 +87,20 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, if login_required and request.user.is_anonymous(): return redirect_to_login(request.path) - mod = models.get_module(app_label, module_name) - # Look up the object to be edited lookup_kwargs = {} if object_id: - lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id + lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field") - lookup_kwargs.update(extra_lookup_kwargs) try: - object = mod.get_object(**lookup_kwargs) + object = model.objects.get(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) + raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs) - manipulator = mod.ChangeManipulator(object.id, follow=follow) + manipulator = model.ChangeManipulator(getattr(object, object._meta.pk.name), follow=follow) if request.POST: new_data = request.POST.copy() @@ -114,7 +110,7 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, manipulator.save(new_data) if not request.user.is_anonymous(): - request.user.add_message("The %s was updated successfully." % mod.Klass._meta.verbose_name) + request.user.message_set.create(message="The %s was updated successfully." % model._meta.verbose_name) # Do a post-after-redirect so that reload works, etc. if post_save_redirect: @@ -128,11 +124,11 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, # This makes sure the form acurate represents the fields of the place. new_data = manipulator.flatten_data() - form = formfields.FormWrapper(manipulator, new_data, errors) + form = forms.FormWrapper(manipulator, new_data, errors) if not template_name: - template_name = "%s/%s_form" % (app_label, module_name) + template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { 'form': form, template_object_name: object, }, context_processors) @@ -142,12 +138,12 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, model, getattr(object, object._meta.pk.name)) return response -def delete_object(request, app_label, module_name, post_delete_redirect, +def delete_object(request, model, post_delete_redirect, object_id=None, slug=None, slug_field=None, template_name=None, - template_loader=loader, extra_lookup_kwargs={}, extra_context={}, + template_loader=loader, extra_context={}, login_required=False, context_processors=None, template_object_name='object'): """ Generic object-delete function. @@ -156,7 +152,7 @@ def delete_object(request, app_label, module_name, post_delete_redirect, fetched using GET; for safty, deletion will only be performed if this view is POSTed. - Templates: ``<app_label>/<module_name>_confirm_delete`` + Templates: ``<app_label>/<model_name>_confirm_delete.html`` Context: object the original object being deleted @@ -164,32 +160,29 @@ def delete_object(request, app_label, module_name, post_delete_redirect, if login_required and request.user.is_anonymous(): return redirect_to_login(request.path) - mod = models.get_module(app_label, module_name) - # Look up the object to be edited lookup_kwargs = {} if object_id: - lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id + lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field") - lookup_kwargs.update(extra_lookup_kwargs) try: - object = mod.get_object(**lookup_kwargs) + object = model._default_manager.get(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) + raise Http404, "No %s found for %s" % (model._meta.app_label, lookup_kwargs) if request.META['REQUEST_METHOD'] == 'POST': object.delete() if not request.user.is_anonymous(): - request.user.add_message("The %s was deleted." % mod.Klass._meta.verbose_name) + request.user.message_set.create(message="The %s was deleted." % model._meta.verbose_name) return HttpResponseRedirect(post_delete_redirect) else: if not template_name: - template_name = "%s/%s_confirm_delete" % (app_label, module_name) + template_name = "%s/%s_confirm_delete.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { template_object_name: object, }, context_processors) for key, value in extra_context.items(): @@ -198,5 +191,5 @@ def delete_object(request, app_label, module_name, post_delete_redirect, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, model, getattr(object, object._meta.pk.name)) return response diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py index 9b9a3034ba..1a6cbc8369 100644 --- a/django/views/generic/date_based.py +++ b/django/views/generic/date_based.py @@ -1,44 +1,37 @@ -from django.core.template import loader -from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.extensions import DjangoContext +from django.template import loader, RequestContext +from django.core.exceptions import ObjectDoesNotExist from django.core.xheaders import populate_xheaders -from django.models import get_module -from django.utils.httpwrappers import HttpResponse +from django.http import Http404, HttpResponse import datetime, time -def archive_index(request, app_label, module_name, date_field, num_latest=15, - template_name=None, template_loader=loader, extra_lookup_kwargs={}, +def archive_index(request, queryset, date_field, num_latest=15, + template_name=None, template_loader=loader, extra_context={}, allow_empty=False, context_processors=None): """ Generic top-level archive of date-based objects. - Templates: ``<app_label>/<module_name>_archive`` + Templates: ``<app_label>/<model_name>_archive.html`` Context: date_list List of years latest Latest N (defaults to 15) objects by date """ - mod = get_module(app_label, module_name) - lookup_kwargs = {'%s__lte' % date_field: datetime.datetime.now()} - lookup_kwargs.update(extra_lookup_kwargs) - date_list = getattr(mod, "get_%s_list" % date_field)('year', **lookup_kwargs)[::-1] + model = queryset.model + queryset = queryset.filter(**{'%s__lte' % date_field: datetime.datetime.now()}) + date_list = queryset.dates(date_field, 'year')[::-1] if not date_list and not allow_empty: - raise Http404("No %s.%s available" % (app_label, module_name)) + raise Http404, "No %s available" % model._meta.verbose_name if date_list and num_latest: - lookup_kwargs.update({ - 'limit': num_latest, - 'order_by': ('-' + date_field,), - }) - latest = mod.get_list(**lookup_kwargs) + latest = queryset.order_by('-'+date_field)[:num_latest] else: latest = None if not template_name: - template_name = "%s/%s_archive" % (app_label, module_name) + template_name = "%s/%s_archive.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { 'date_list' : date_list, 'latest' : latest, }, context_processors) @@ -49,33 +42,34 @@ def archive_index(request, app_label, module_name, date_field, num_latest=15, c[key] = value return HttpResponse(t.render(c)) -def archive_year(request, year, app_label, module_name, date_field, - template_name=None, template_loader=loader, extra_lookup_kwargs={}, - extra_context={}, allow_empty=False, context_processors=None): +def archive_year(request, year, queryset, date_field, template_name=None, + template_loader=loader, extra_context={}, allow_empty=False, + context_processors=None): """ Generic yearly archive view. - Templates: ``<app_label>/<module_name>_archive_year`` + Templates: ``<app_label>/<model_name>_archive_year.html`` Context: date_list List of months in this year with objects year This year """ - mod = get_module(app_label, module_name) + model = queryset.model now = datetime.datetime.now() + lookup_kwargs = {'%s__year' % date_field: year} + # Only bother to check current date if the year isn't in the past. if int(year) >= now.year: lookup_kwargs['%s__lte' % date_field] = now - lookup_kwargs.update(extra_lookup_kwargs) - date_list = getattr(mod, "get_%s_list" % date_field)('month', **lookup_kwargs) + date_list = queryset.filter(**lookup_kwargs).dates(date_field, 'month') if not date_list and not allow_empty: raise Http404 if not template_name: - template_name = "%s/%s_archive_year" % (app_label, module_name) + template_name = "%s/%s_archive_year.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { 'date_list': date_list, 'year': year, }, context_processors) @@ -86,14 +80,14 @@ def archive_year(request, year, app_label, module_name, date_field, c[key] = value return HttpResponse(t.render(c)) -def archive_month(request, year, month, app_label, module_name, date_field, +def archive_month(request, year, month, queryset, date_field, month_format='%b', template_name=None, template_loader=loader, - extra_lookup_kwargs={}, extra_context={}, allow_empty=False, - context_processors=None, template_object_name='object'): + extra_context={}, allow_empty=False, context_processors=None, + template_object_name='object'): """ Generic monthly archive view. - Templates: ``<app_label>/<module_name>_archive_month`` + Templates: ``<app_label>/<model_name>_archive_month.html`` Context: month: (date) this month @@ -109,8 +103,9 @@ def archive_month(request, year, month, app_label, module_name, date_field, except ValueError: raise Http404 - mod = get_module(app_label, module_name) + model = queryset.model now = datetime.datetime.now() + # Calculate first and last day of month, for use in a date-range lookup. first_day = date.replace(day=1) if first_day.month == 12: @@ -118,17 +113,17 @@ def archive_month(request, year, month, app_label, module_name, date_field, else: last_day = first_day.replace(month=first_day.month + 1) lookup_kwargs = {'%s__range' % date_field: (first_day, last_day)} + # Only bother to check current date if the month isn't in the past. if last_day >= now.date(): lookup_kwargs['%s__lte' % date_field] = now - lookup_kwargs.update(extra_lookup_kwargs) - object_list = mod.get_list(**lookup_kwargs) + object_list = queryset.filter(**lookup_kwargs) if not object_list and not allow_empty: raise Http404 if not template_name: - template_name = "%s/%s_archive_month" % (app_label, module_name) + template_name = "%s/%s_archive_month.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { '%s_list' % template_object_name: object_list, 'month': date, 'next_month': (last_day < datetime.date.today()) and (last_day + datetime.timedelta(days=1)) or None, @@ -141,14 +136,61 @@ def archive_month(request, year, month, app_label, module_name, date_field, c[key] = value return HttpResponse(t.render(c)) -def archive_day(request, year, month, day, app_label, module_name, date_field, +def archive_week(request, year, week, queryset, date_field, + template_name=None, template_loader=loader, + extra_context={}, allow_empty=True, context_processors=None, + template_object_name='object'): + """ + Generic weekly archive view. + + Templates: ``<app_label>/<model_name>_archive_week.html`` + Context: + week: + (date) this week + object_list: + list of objects published in the given week + """ + try: + date = datetime.date(*time.strptime(year+'-0-'+week, '%Y-%w-%U')[:3]) + except ValueError: + raise Http404 + + model = queryset.model + now = datetime.datetime.now() + + # Calculate first and last day of week, for use in a date-range lookup. + first_day = date + last_day = date + datetime.timedelta(days=7) + lookup_kwargs = {'%s__range' % date_field: (first_day, last_day)} + + # Only bother to check current date if the week isn't in the past. + if last_day >= now.date(): + lookup_kwargs['%s__lte' % date_field] = now + object_list = queryset.filter(**lookup_kwargs) + if not object_list and not allow_empty: + raise Http404 + if not template_name: + template_name = "%s/%s_archive_week.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + '%s_list' % template_object_name: object_list, + 'week': date, + }) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + return HttpResponse(t.render(c)) + +def archive_day(request, year, month, day, queryset, date_field, month_format='%b', day_format='%d', template_name=None, - template_loader=loader, extra_lookup_kwargs={}, extra_context={}, - allow_empty=False, context_processors=None, template_object_name='object'): + template_loader=loader, extra_context={}, allow_empty=False, + context_processors=None, template_object_name='object'): """ Generic daily archive view. - Templates: ``<app_label>/<module_name>_archive_day`` + Templates: ``<app_label>/<model_name>_archive_day.html`` Context: object_list: list of objects published that day @@ -164,22 +206,23 @@ def archive_day(request, year, month, day, app_label, module_name, date_field, except ValueError: raise Http404 - mod = get_module(app_label, module_name) + model = queryset.model now = datetime.datetime.now() + lookup_kwargs = { '%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max)), } + # Only bother to check current date if the date isn't in the past. if date >= now.date(): lookup_kwargs['%s__lte' % date_field] = now - lookup_kwargs.update(extra_lookup_kwargs) - object_list = mod.get_list(**lookup_kwargs) + object_list = queryset.filter(**lookup_kwargs) if not allow_empty and not object_list: raise Http404 if not template_name: - template_name = "%s/%s_archive_day" % (app_label, module_name) + template_name = "%s/%s_archive_day.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) - c = DjangoContext(request, { + c = RequestContext(request, { '%s_list' % template_object_name: object_list, 'day': date, 'previous_day': date - datetime.timedelta(days=1), @@ -204,15 +247,15 @@ def archive_today(request, **kwargs): }) return archive_day(request, **kwargs) -def object_detail(request, year, month, day, app_label, module_name, date_field, +def object_detail(request, year, month, day, queryset, date_field, month_format='%b', day_format='%d', object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, - template_loader=loader, extra_lookup_kwargs={}, extra_context={}, - context_processors=None, template_object_name='object'): + template_loader=loader, extra_context={}, context_processors=None, + template_object_name='object'): """ Generic detail view from year/month/day/slug or year/month/day/id structure. - Templates: ``<app_label>/<module_name>_detail`` + Templates: ``<app_label>/<model_name>_detail.html`` Context: object: the object to be detailed @@ -222,34 +265,35 @@ def object_detail(request, year, month, day, app_label, module_name, date_field, except ValueError: raise Http404 - mod = get_module(app_label, module_name) + model = queryset.model now = datetime.datetime.now() + lookup_kwargs = { '%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max)), } + # Only bother to check current date if the date isn't in the past. if date >= now.date(): lookup_kwargs['%s__lte' % date_field] = now if object_id: - lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id + lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: - raise AttributeError("Generic detail view must be called with either an object_id or a slug/slugfield") - lookup_kwargs.update(extra_lookup_kwargs) + raise AttributeError, "Generic detail view must be called with either an object_id or a slug/slugfield" try: - object = mod.get_object(**lookup_kwargs) + obj = queryset.get(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) + raise Http404, "No %s found for" % model._meta.verbose_name if not template_name: - template_name = "%s/%s_detail" % (app_label, module_name) + template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower()) if template_name_field: - template_name_list = [getattr(object, template_name_field), template_name] + template_name_list = [getattr(obj, template_name_field), template_name] t = template_loader.select_template(template_name_list) else: t = template_loader.get_template(template_name) - c = DjangoContext(request, { - template_object_name: object, + c = RequestContext(request, { + template_object_name: obj, }, context_processors) for key, value in extra_context.items(): if callable(value): @@ -257,5 +301,5 @@ def object_detail(request, year, month, day, app_label, module_name, date_field, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name)) return response diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index 5cc496f5ed..68a1e73b07 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -1,18 +1,16 @@ -from django import models -from django.core.template import loader -from django.utils.httpwrappers import HttpResponse +from django.template import loader, RequestContext +from django.http import Http404, HttpResponse from django.core.xheaders import populate_xheaders -from django.core.extensions import DjangoContext from django.core.paginator import ObjectPaginator, InvalidPage -from django.core.exceptions import Http404, ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist -def object_list(request, app_label, module_name, paginate_by=None, allow_empty=False, - template_name=None, template_loader=loader, extra_lookup_kwargs={}, +def object_list(request, queryset, paginate_by=None, allow_empty=False, + template_name=None, template_loader=loader, extra_context={}, context_processors=None, template_object_name='object'): """ Generic list of objects. - Templates: ``<app_label>/<module_name>_list`` + Templates: ``<app_label>/<model_name>_list.html`` Context: object_list list of objects @@ -35,10 +33,10 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F hits number of objects, total """ - mod = models.get_module(app_label, module_name) - lookup_kwargs = extra_lookup_kwargs.copy() + queryset = queryset._clone() + model = queryset.model if paginate_by: - paginator = ObjectPaginator(mod, lookup_kwargs, paginate_by) + paginator = ObjectPaginator(queryset, paginate_by) page = request.GET.get('page', 1) try: page = int(page) @@ -48,7 +46,7 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F object_list = [] else: raise Http404 - c = DjangoContext(request, { + c = RequestContext(request, { '%s_list' % template_object_name: object_list, 'is_paginated': paginator.pages > 1, 'results_per_page': paginate_by, @@ -61,12 +59,11 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F 'hits' : paginator.hits, }, context_processors) else: - object_list = mod.get_list(**lookup_kwargs) - c = DjangoContext(request, { - '%s_list' % template_object_name: object_list, + c = RequestContext(request, { + '%s_list' % template_object_name: queryset, 'is_paginated': False }, context_processors) - if len(object_list) == 0 and not allow_empty: + if not allow_empty and len(queryset) == 0: raise Http404 for key, value in extra_context.items(): if callable(value): @@ -74,44 +71,42 @@ def object_list(request, app_label, module_name, paginate_by=None, allow_empty=F else: c[key] = value if not template_name: - template_name = "%s/%s_list" % (app_label, module_name) + template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower()) t = template_loader.get_template(template_name) return HttpResponse(t.render(c)) -def object_detail(request, app_label, module_name, object_id=None, slug=None, +def object_detail(request, queryset, object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, - template_loader=loader, extra_lookup_kwargs={}, extra_context={}, + template_loader=loader, extra_context={}, context_processors=None, template_object_name='object'): """ Generic list of objects. - Templates: ``<app_label>/<module_name>_detail`` + Templates: ``<app_label>/<model_name>_detail.html`` Context: object the object """ - mod = models.get_module(app_label, module_name) - lookup_kwargs = {} + model = queryset.model if object_id: - lookup_kwargs['pk'] = object_id + queryset = queryset.filter(pk=object_id) elif slug and slug_field: - lookup_kwargs['%s__exact' % slug_field] = slug + queryset = queryset.filter(**{slug_field: slug}) else: - raise AttributeError("Generic detail view must be called with either an object_id or a slug/slug_field") - lookup_kwargs.update(extra_lookup_kwargs) + raise AttributeError, "Generic detail view must be called with either an object_id or a slug/slug_field." try: - object = mod.get_object(**lookup_kwargs) + obj = queryset.get() except ObjectDoesNotExist: - raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) + raise Http404, "No %s found matching the query" % (model._meta.verbose_name) if not template_name: - template_name = "%s/%s_detail" % (app_label, module_name) + template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower()) if template_name_field: - template_name_list = [getattr(object, template_name_field), template_name] + template_name_list = [getattr(obj, template_name_field), template_name] t = template_loader.select_template(template_name_list) else: t = template_loader.get_template(template_name) - c = DjangoContext(request, { - template_object_name: object, + c = RequestContext(request, { + template_object_name: obj, }, context_processors) for key, value in extra_context.items(): if callable(value): @@ -119,5 +114,5 @@ def object_detail(request, app_label, module_name, object_id=None, slug=None, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name)) return response diff --git a/django/views/generic/simple.py b/django/views/generic/simple.py index 086ed42805..4571ef8605 100644 --- a/django/views/generic/simple.py +++ b/django/views/generic/simple.py @@ -1,12 +1,13 @@ -from django.core.extensions import DjangoContext, render_to_response -from django.utils.httpwrappers import HttpResponse, HttpResponsePermanentRedirect, HttpResponseGone +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.http import HttpResponse, HttpResponsePermanentRedirect, HttpResponseGone def direct_to_template(request, template, **kwargs): """ Render a given template with any extra URL parameters in the context as ``{{ params }}``. """ - return render_to_response(template, {'params' : kwargs}, context_instance=DjangoContext(request)) + return render_to_response(template, {'params' : kwargs}, context_instance=RequestContext(request)) def redirect_to(request, url, **kwargs): """ diff --git a/django/views/i18n.py b/django/views/i18n.py index 2f3b2b2d31..a2bc54c9ed 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -1,4 +1,4 @@ -from django.utils import httpwrappers +from django import http from django.utils.translation import check_for_language, activate, to_locale, get_language from django.utils.text import javascript_quote from django.conf import settings @@ -17,7 +17,7 @@ def set_language(request): next = request.META.get('HTTP_REFERER', None) if not next: next = '/' - response = httpwrappers.HttpResponseRedirect(next) + response = http.HttpResponseRedirect(next) if check_for_language(lang_code): if hasattr(request, 'session'): request.session['django_language'] = lang_code @@ -190,5 +190,5 @@ def javascript_catalog(request, domain='djangojs', packages=None): src.append(LibFoot) src.append(InterPolate) src = ''.join(src) - return httpwrappers.HttpResponse(src, 'text/javascript') + return http.HttpResponse(src, 'text/javascript') diff --git a/django/views/registration/passwords.py b/django/views/registration/passwords.py deleted file mode 100644 index 65bacafd98..0000000000 --- a/django/views/registration/passwords.py +++ /dev/null @@ -1,100 +0,0 @@ -from django.core import formfields, validators -from django.core.extensions import DjangoContext, render_to_response -from django.core.template import Context, loader -from django.models.auth import users -from django.views.decorators.auth import login_required -from django.utils.httpwrappers import HttpResponseRedirect - -class PasswordResetForm(formfields.Manipulator): - "A form that lets a user request a password reset" - def __init__(self): - self.fields = ( - formfields.EmailField(field_name="email", length=40, is_required=True, - validator_list=[self.isValidUserEmail]), - ) - - def isValidUserEmail(self, new_data, all_data): - "Validates that a user exists with the given e-mail address" - try: - self.user_cache = users.get_object(email__iexact=new_data) - except users.UserDoesNotExist: - raise validators.ValidationError, "That e-mail address doesn't have an associated user acount. Are you sure you've registered?" - - def save(self, domain_override=None): - "Calculates a new password randomly and sends it to the user" - from django.core.mail import send_mail - from django.models.core import sites - new_pass = users.make_random_password() - self.user_cache.set_password(new_pass) - self.user_cache.save() - if not domain_override: - current_site = sites.get_current() - site_name = current_site.name - domain = current_site.domain - else: - site_name = domain = domain_override - t = loader.get_template('registration/password_reset_email') - c = { - 'new_password': new_pass, - 'email': self.user_cache.email, - 'domain': domain, - 'site_name': site_name, - 'user': self.user_cache, - } - send_mail('Password reset on %s' % site_name, t.render(Context(c)), None, [self.user_cache.email]) - -class PasswordChangeForm(formfields.Manipulator): - "A form that lets a user change his password." - def __init__(self, user): - self.user = user - self.fields = ( - formfields.PasswordField(field_name="old_password", length=30, maxlength=30, is_required=True, - validator_list=[self.isValidOldPassword]), - formfields.PasswordField(field_name="new_password1", length=30, maxlength=30, is_required=True, - validator_list=[validators.AlwaysMatchesOtherField('new_password2', "The two 'new password' fields didn't match.")]), - formfields.PasswordField(field_name="new_password2", length=30, maxlength=30, is_required=True), - ) - - def isValidOldPassword(self, new_data, all_data): - "Validates that the old_password field is correct." - if not self.user.check_password(new_data): - raise validators.ValidationError, "Your old password was entered incorrectly. Please enter it again." - - def save(self, new_data): - "Saves the new password." - self.user.set_password(new_data['new_password1']) - self.user.save() - -def password_reset(request, is_admin_site=False): - new_data, errors = {}, {} - form = PasswordResetForm() - if request.POST: - new_data = request.POST.copy() - errors = form.get_validation_errors(new_data) - if not errors: - if is_admin_site: - form.save(request.META['HTTP_HOST']) - else: - form.save() - return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response('registration/password_reset_form', {'form': formfields.FormWrapper(form, new_data, errors)}, - context_instance=DjangoContext(request)) - -def password_reset_done(request): - return render_to_response('registration/password_reset_done', context_instance=DjangoContext(request)) - -def password_change(request): - new_data, errors = {}, {} - form = PasswordChangeForm(request.user) - if request.POST: - new_data = request.POST.copy() - errors = form.get_validation_errors(new_data) - if not errors: - form.save(new_data) - return HttpResponseRedirect('%sdone/' % request.path) - return render_to_response('registration/password_change_form', {'form': formfields.FormWrapper(form, new_data, errors)}, - context_instance=DjangoContext(request)) -password_change = login_required(password_change) - -def password_change_done(request): - return render_to_response('registration/password_change_done', context_instance=DjangoContext(request)) diff --git a/django/views/static.py b/django/views/static.py index 1499dd4847..072a01671e 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -1,15 +1,14 @@ +from django.template import loader +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified +from django.template import Template, Context, TemplateDoesNotExist +import mimetypes import os -import urllib import posixpath -import mimetypes import re import rfc822 import stat -from django.core import template_loader -from django.core.exceptions import Http404, ImproperlyConfigured -from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect, \ - HttpResponseNotModified -from django.core.template import Template, Context, TemplateDoesNotExist +import urllib def serve(request, path, document_root=None, show_indexes=False): """ @@ -81,7 +80,7 @@ DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ def directory_index(path, fullpath): try: - t = template_loader.get_template('static/directory_index') + t = loader.get_template('static/directory_index') except TemplateDoesNotExist: t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE) files = [] |
