summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Holovaty <adrian@holovaty.com>2005-07-13 01:25:57 +0000
committerAdrian Holovaty <adrian@holovaty.com>2005-07-13 01:25:57 +0000
commited114e15106192b22ebb78ef5bf5bce72b419d13 (patch)
treef7c27f035cca8d50bd69e2ecbd7497fccec4a35a
parent07ffc7d605cc96557db28a9e35da69bc0719611b (diff)
downloaddjango-ed114e15106192b22ebb78ef5bf5bce72b419d13.tar.gz
Imported Django from private SVN repository (created from r. 8825)
git-svn-id: http://code.djangoproject.com/svn/django/trunk@3 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/__init__.py0
-rw-r--r--django/bin/__init__.py0
-rw-r--r--django/bin/daily_cleanup.py15
-rw-r--r--django/bin/django-admin.py412
-rw-r--r--django/bin/profiling/__init__.py0
-rw-r--r--django/bin/profiling/gather_profile_stats.py34
-rw-r--r--django/bin/profiling/handler.py22
-rw-r--r--django/bin/setup.py45
-rw-r--r--django/bin/validate.py36
-rw-r--r--django/conf/__init__.py0
-rw-r--r--django/conf/app_template/__init__.py0
-rw-r--r--django/conf/app_template/models/__init__.py1
-rw-r--r--django/conf/app_template/models/app_name.py3
-rw-r--r--django/conf/app_template/urls/__init__.py0
-rw-r--r--django/conf/app_template/urls/app_name.py5
-rw-r--r--django/conf/app_template/views/__init__.py0
-rw-r--r--django/conf/global_settings.py199
-rw-r--r--django/conf/project_template/__init__.py0
-rw-r--r--django/conf/project_template/apps/__init__.py0
-rw-r--r--django/conf/project_template/settings/__init__.py0
-rw-r--r--django/conf/project_template/settings/main.py31
-rw-r--r--django/conf/settings.py42
-rw-r--r--django/conf/urls/__init__.py0
-rw-r--r--django/conf/urls/admin.py56
-rw-r--r--django/conf/urls/admin_password_reset.py6
-rw-r--r--django/conf/urls/comments.py12
-rw-r--r--django/conf/urls/defaults.py17
-rw-r--r--django/conf/urls/flatfiles.py5
-rw-r--r--django/conf/urls/registration.py19
-rw-r--r--django/conf/urls/rss.py6
-rw-r--r--django/conf/urls/shortcut.py5
-rw-r--r--django/core/__init__.py0
-rw-r--r--django/core/cache.py255
-rw-r--r--django/core/db/__init__.py28
-rw-r--r--django/core/db/backends/__init__.py0
-rw-r--r--django/core/db/backends/mysql.py107
-rw-r--r--django/core/db/backends/postgresql.py109
-rw-r--r--django/core/db/base.py32
-rw-r--r--django/core/db/typecasts.py42
-rw-r--r--django/core/defaultfilters.py466
-rw-r--r--django/core/defaulttags.py743
-rw-r--r--django/core/exceptions.py26
-rw-r--r--django/core/extensions.py79
-rw-r--r--django/core/formfields.py759
-rw-r--r--django/core/handler.py157
-rw-r--r--django/core/mail.py51
-rw-r--r--django/core/meta.py2142
-rw-r--r--django/core/paginator.py76
-rw-r--r--django/core/rss.py136
-rw-r--r--django/core/template.py488
-rw-r--r--django/core/template_file.py18
-rw-r--r--django/core/template_loader.py142
-rw-r--r--django/core/urlresolvers.py96
-rw-r--r--django/core/validators.py420
-rw-r--r--django/core/xheaders.py22
-rw-r--r--django/middleware/__init__.py0
-rw-r--r--django/middleware/admin.py120
-rw-r--r--django/middleware/common.py104
-rw-r--r--django/middleware/doc.py18
-rw-r--r--django/models/__init__.py91
-rw-r--r--django/models/auth.py290
-rw-r--r--django/models/comments.py281
-rw-r--r--django/models/core.py107
-rw-r--r--django/parts/__init__.py0
-rw-r--r--django/parts/admin/__init__.py0
-rw-r--r--django/parts/admin/doc.py93
-rw-r--r--django/parts/auth/__init__.py0
-rw-r--r--django/parts/auth/anonymoususers.py48
-rw-r--r--django/parts/auth/formfields.py46
-rw-r--r--django/parts/media/__init__.py0
-rw-r--r--django/parts/media/photos.py6
-rw-r--r--django/templatetags/__init__.py7
-rw-r--r--django/templatetags/comments.py331
-rw-r--r--django/templatetags/log.py45
-rw-r--r--django/tests/__init__.py0
-rw-r--r--django/tests/cache_tests.py119
-rw-r--r--django/tests/template_inheritance.py102
-rw-r--r--django/tests/template_tests.py707
-rw-r--r--django/utils/__init__.py0
-rw-r--r--django/utils/datastructures.py171
-rw-r--r--django/utils/dateformat.py317
-rw-r--r--django/utils/dates.py27
-rw-r--r--django/utils/feedgenerator.py152
-rw-r--r--django/utils/html.py110
-rw-r--r--django/utils/httpwrappers.py319
-rw-r--r--django/utils/images.py22
-rw-r--r--django/utils/stopwords.py42
-rw-r--r--django/utils/text.py108
-rw-r--r--django/utils/timesince.py46
-rw-r--r--django/utils/xmlutils.py13
-rw-r--r--django/views/__init__.py0
-rw-r--r--django/views/admin/__init__.py0
-rw-r--r--django/views/admin/doc.py328
-rw-r--r--django/views/admin/main.py1089
-rw-r--r--django/views/admin/template.py70
-rw-r--r--django/views/auth/__init__.py0
-rw-r--r--django/views/auth/login.py62
-rw-r--r--django/views/comments/__init__.py0
-rw-r--r--django/views/comments/comments.py347
-rw-r--r--django/views/comments/karma.py34
-rw-r--r--django/views/comments/userflags.py82
-rw-r--r--django/views/core/__init__.py0
-rw-r--r--django/views/core/flatfiles.py34
-rw-r--r--django/views/decorators/__init__.py0
-rw-r--r--django/views/decorators/auth.py12
-rw-r--r--django/views/decorators/cache.py64
-rw-r--r--django/views/defaults.py72
-rw-r--r--django/views/generic/__init__.py0
-rw-r--r--django/views/generic/date_based.py223
-rw-r--r--django/views/generic/list_detail.py106
-rw-r--r--django/views/registration/__init__.py0
-rw-r--r--django/views/registration/passwords.py109
-rw-r--r--django/views/rss/__init__.py0
-rw-r--r--django/views/rss/rss.py12
114 files changed, 13851 insertions, 0 deletions
diff --git a/django/__init__.py b/django/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/__init__.py
diff --git a/django/bin/__init__.py b/django/bin/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/bin/__init__.py
diff --git a/django/bin/daily_cleanup.py b/django/bin/daily_cleanup.py
new file mode 100644
index 0000000000..b7235cd10c
--- /dev/null
+++ b/django/bin/daily_cleanup.py
@@ -0,0 +1,15 @@
+"Daily cleanup file"
+
+from django.core.db import db
+
+DOCUMENTATION_DIRECTORY = '/home/html/documentation/'
+
+def clean_up():
+ # Clean up old database records
+ cursor = db.cursor()
+ cursor.execute("DELETE FROM auth_sessions WHERE start_time < NOW() - INTERVAL '2 weeks'")
+ cursor.execute("DELETE FROM registration_challenges WHERE request_date < NOW() - INTERVAL '1 week'")
+ db.commit()
+
+if __name__ == "__main__":
+ clean_up()
diff --git a/django/bin/django-admin.py b/django/bin/django-admin.py
new file mode 100644
index 0000000000..fd2d96ad28
--- /dev/null
+++ b/django/bin/django-admin.py
@@ -0,0 +1,412 @@
+#!/usr/bin/python2.3
+from django.core import db, meta
+import django
+import os, re, sys
+
+MODULE_TEMPLATE = ''' {%% if perms.%(app)s.%(addperm)s or perms.%(app)s.%(changeperm)s %%}
+ <tr>
+ <th>{%% if perms.%(app)s.%(changeperm)s %%}<a href="/%(app)s/%(mod)s/">{%% endif %%}%(name)s{%% if perms.%(app)s.%(changeperm)s %%}</a>{%% endif %%}</th>
+ <td class="x50">{%% if perms.%(app)s.%(addperm)s %%}<a href="/%(app)s/%(mod)s/add/" class="addlink">{%% endif %%}Add{%% if perms.%(app)s.%(addperm)s %%}</a>{%% endif %%}</td>
+ <td class="x75">{%% if perms.%(app)s.%(changeperm)s %%}<a href="/%(app)s/%(mod)s/" class="changelink">{%% endif %%}Change{%% if perms.%(app)s.%(changeperm)s %%}</a>{%% endif %%}</td>
+ </tr>
+ {%% endif %%}'''
+
+APP_ARGS = '[app app ...]'
+
+PROJECT_TEMPLATE_DIR = django.__path__[0] + '/conf/%s_template'
+
+def _get_packages_insert(app_label):
+ return "INSERT INTO packages (label, name) VALUES ('%s', '%s');" % (app_label, app_label)
+
+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 = []
+ 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):
+ return "INSERT INTO auth_permissions (name, package, codename) VALUES ('%s', '%s', '%s');" % \
+ (name.replace("'", "''"), opts.app_label, codename)
+
+def _get_contenttype_insert(opts):
+ return "INSERT INTO content_types (name, package, python_module_name) VALUES ('%s', '%s', '%s');" % \
+ (opts.verbose_name, opts.app_label, opts.module_name)
+
+def _is_valid_dir_name(s):
+ return bool(re.search(r'^\w+$', s))
+
+def get_sql_create(mod):
+ "Returns a list of the CREATE TABLE SQL statements for the given module."
+ 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.to.get_field(f.rel.field_name)
+ # If the foreign key points to an AutoField, the foreign key
+ # should be an IntegerField, not an AutoField. Otherwise, the
+ # foreign key should be the same type of field as the field
+ # to which it points.
+ if rel_field.__class__.__name__ == 'AutoField':
+ data_type = 'IntegerField'
+ else:
+ rel_field.__class__.__name__
+ else:
+ rel_field = f
+ data_type = f.__class__.__name__
+ col_type = db.DATA_TYPES[data_type]
+ if col_type is not None:
+ field_output = [f.name, 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)' % \
+ (f.rel.to.db_table, f.rel.to.get_field(f.rel.field_name).name))
+ table_output.append(' '.join(field_output))
+ if opts.order_with_respect_to:
+ table_output.append('_order %s NULL' % db.DATA_TYPES['IntegerField'])
+ for field_constraints in opts.unique_together:
+ table_output.append('UNIQUE (%s)' % ", ".join(field_constraints))
+
+ full_statement = ['CREATE TABLE %s (' % 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))
+
+ for klass in mod._MODELS:
+ opts = klass._meta
+ for f in opts.many_to_many:
+ table_output = ['CREATE TABLE %s_%s (' % (opts.db_table, f.name)]
+ table_output.append(' id %s NOT NULL PRIMARY KEY,' % db.DATA_TYPES['AutoField'])
+ table_output.append(' %s_id %s NOT NULL REFERENCES %s (%s),' % \
+ (opts.object_name.lower(), db.DATA_TYPES['IntegerField'], opts.db_table, opts.pk.name))
+ table_output.append(' %s_id %s NOT NULL REFERENCES %s (%s),' % \
+ (f.rel.to.object_name.lower(), db.DATA_TYPES['IntegerField'], f.rel.to.db_table, f.rel.to.pk.name))
+ table_output.append(' UNIQUE (%s_id, %s_id)' % (opts.object_name.lower(), f.rel.to.object_name.lower()))
+ 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 app(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."
+ try:
+ cursor = db.db.cursor()
+ except:
+ cursor = None
+ output = []
+ for klass in mod._MODELS:
+ try:
+ if cursor is not None:
+ # Check whether the table exists.
+ cursor.execute("SELECT 1 FROM %s LIMIT 1" % klass._meta.db_table)
+ except:
+ # The table doesn't exist, so it doesn't need to be dropped.
+ pass
+ else:
+ output.append("DROP TABLE %s;" % klass._meta.db_table)
+ for klass in mod._MODELS:
+ opts = klass._meta
+ for f in opts.many_to_many:
+ try:
+ if cursor is not None:
+ cursor.execute("SELECT 1 FROM %s_%s LIMIT 1" % (opts.db_table, f.name))
+ except:
+ pass
+ else:
+ output.append("DROP TABLE %s_%s;" % (opts.db_table, f.name))
+ output.append("DELETE FROM packages WHERE label = '%s';" % mod._MODELS[0]._meta.app_label)
+ return output
+get_sql_delete.help_doc = "Prints the DROP TABLE SQL statements for the given app(s)."
+get_sql_delete.args = APP_ARGS
+
+def get_sql_reset(mod):
+ "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 app(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."
+ 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_file_name = os.path.join(app_dir, opts.module_name + '.sql')
+ if os.path.exists(sql_file_name):
+ fp = open(sql_file_name, 'r')
+ 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 app(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."
+ output = []
+ for klass in mod._MODELS:
+ 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.name, f.name, klass._meta.db_table))
+ return output
+get_sql_sequence_reset.help_doc = "Prints the SQL statements for resetting PostgreSQL sequences for the given app(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."
+ output = []
+ for klass in mod._MODELS:
+ 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.name, klass._meta.db_table, f.name))
+ return output
+get_sql_indexes.help_doc = "Prints the CREATE INDEX SQL statements for the given app(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 app(s)."
+get_sql_all.args = APP_ARGS
+
+def database_check(mod):
+ "Checks that everything is properly installed in the database for the given module."
+ cursor = db.db.cursor()
+ app_label = mod._MODELS[0]._meta.app_label
+
+ # Check that the package exists in the database.
+ cursor.execute("SELECT 1 FROM packages WHERE label = %s", [app_label])
+ if cursor.rowcount < 1:
+# sys.stderr.write("The '%s' package isn't installed.\n" % app_label)
+ print _get_packages_insert(app_label)
+
+ # 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 auth_permissions WHERE package = %s AND codename = %s", (app_label, codename))
+ if cursor.rowcount < 1:
+# 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 content_types WHERE package = %s AND python_module_name = %s", (app_label, opts.module_name))
+ if cursor.rowcount < 1:
+# sys.stderr.write("The '%s.%s' content type doesn't exist.\n" % (app_label, opts.module_name))
+ print _get_contenttype_insert(opts)
+
+ # Check that there aren't any *extra* permissions in the DB that the model
+ # doesn't know about.
+ cursor.execute("SELECT codename FROM auth_permissions WHERE package = %s", (app_label,))
+ for row in cursor.fetchall():
+ 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 auth_permissions WHERE package='%s' AND codename = '%s';" % (app_label, row[0])
+
+ # Check that there aren't any *extra* content types in the DB that the
+ # model doesn't know about.
+ cursor.execute("SELECT python_module_name FROM content_types WHERE package = %s", (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 content_types WHERE package='%s' AND python_module_name = '%s';" % (app_label, row[0])
+database_check.help_doc = "Checks that everything is installed in the database for the given app(s) and prints SQL statements if needed."
+database_check.args = APP_ARGS
+
+def get_admin_index(mod):
+ "Returns admin-index template snippet (in list form) for the given module."
+ output = []
+ app_label = mod._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:
+ if klass._meta.admin:
+ output.append(MODULE_TEMPLATE % {
+ 'app': app_label,
+ 'mod': klass._meta.module_name,
+ 'name': meta.capfirst(klass._meta.verbose_name_plural),
+ 'addperm': klass._meta.get_add_permission(),
+ 'changeperm': klass._meta.get_change_permission(),
+ })
+ output.append('</table></div>')
+ output.append('{% endif %}')
+ return output
+get_admin_index.help_doc = "Prints the admin-index template snippet for the given app(s)."
+get_admin_index.args = APP_ARGS
+
+def init():
+ "Initializes the database with auth and core."
+ auth = meta.get_app('auth')
+ core = meta.get_app('core')
+ try:
+ 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)
+ except Exception, e:
+ sys.stderr.write("Error: The database couldn't be initialized. Here's the full exception:\n%s\n" % e)
+ db.db.rollback()
+ sys.exit(1)
+ db.db.commit()
+init.args = ''
+
+def install(mod):
+ "Executes the equivalent of 'get_sql_all' in the current database."
+ sql_list = get_sql_all(mod)
+ try:
+ cursor = db.db.cursor()
+ for sql in sql_list:
+ cursor.execute(sql)
+ except Exception, e:
+ mod_name = mod.__name__[mod.__name__.rindex('.')+1:]
+ sys.stderr.write("""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 '%s sqlall %s'. That's the SQL this command wasn't able to run.
+The full error: %s\n""" % \
+ (mod_name, __file__, mod_name, e))
+ db.db.rollback()
+ sys.exit(1)
+ db.db.commit()
+install.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.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.exit(1)
+ template_dir = PROJECT_TEMPLATE_DIR % app_or_project
+ for d, subdirs, files in os.walk(template_dir):
+ relative_dir = d[len(template_dir)+1:].replace('%s_name' % app_or_project, name)
+ if relative_dir:
+ os.mkdir(os.path.join(top_dir, relative_dir))
+ for f in files:
+ 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')
+ 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()
+
+def startproject(project_name, directory):
+ "Creates a Django project for the given project_name in the given directory."
+ _start_helper('project', project_name, directory)
+startproject.help_doc = "Creates a Django project directory structure for the given project name in the current directory."
+startproject.args = "[projectname]"
+
+def startapp(app_name, directory):
+ "Creates a Django app for the given project_name in the given directory."
+ # Determine the project_name a bit naively -- by looking at the name of
+ # the parent directory.
+ project_dir = os.path.normpath(os.path.join(directory, '../'))
+ project_name = os.path.basename(project_dir)
+ _start_helper('app', app_name, directory, project_name)
+ settings_file = os.path.join(project_dir, 'settings/main.py')
+ if os.path.exists(settings_file):
+ try:
+ settings_contents = open(settings_file, 'r').read()
+ fp = open(settings_file, 'w')
+ except IOError:
+ pass
+ else:
+ settings_contents = re.sub(r'(?s)\b(INSTALLED_APPS\s*=\s*\()(.*?)\)', "\\1\n '%s',\\2)" % app_name, settings_contents)
+ fp.write(settings_contents)
+ fp.close()
+startapp.help_doc = "Creates a Django app directory structure for the given app name in the current directory."
+startapp.args = "[appname]"
+
+def usage():
+ sys.stderr.write("Usage: %s [action]\n" % sys.argv[0])
+
+ available_actions = ACTION_MAPPING.keys()
+ available_actions.sort()
+ sys.stderr.write("Available actions:\n")
+ for a in available_actions:
+ func = ACTION_MAPPING[a]
+ sys.stderr.write(" %s %s-- %s\n" % (a, func.args, getattr(func, 'help_doc', func.__doc__)))
+ sys.exit(1)
+
+ACTION_MAPPING = {
+ 'adminindex': get_admin_index,
+# 'dbcheck': database_check,
+ 'sql': get_sql_create,
+ 'sqlall': get_sql_all,
+ 'sqlclear': get_sql_delete,
+ 'sqlindexes': get_sql_indexes,
+ 'sqlinitialdata': get_sql_initial_data,
+ 'sqlreset': get_sql_reset,
+ 'sqlsequencereset': get_sql_sequence_reset,
+ 'startapp': startapp,
+ 'startproject': startproject,
+ 'init': init,
+ 'install': install,
+}
+
+if __name__ == "__main__":
+ try:
+ action = sys.argv[1]
+ except IndexError:
+ usage()
+ if not ACTION_MAPPING.has_key(action):
+ usage()
+ if action == 'init':
+ init()
+ sys.exit(0)
+ elif action in ('startapp', 'startproject'):
+ try:
+ name = sys.argv[2]
+ except IndexError:
+ usage()
+ ACTION_MAPPING[action](name, os.getcwd())
+ sys.exit(0)
+ elif action == 'dbcheck':
+ mod_list = meta.get_all_installed_modules()
+ else:
+ try:
+ mod_list = [meta.get_app(app_label) for app_label in sys.argv[2:]]
+ 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:
+ usage()
+ if action not in ('adminindex', 'dbcheck', 'install', 'sqlindexes'):
+ print "BEGIN;"
+ for mod in mod_list:
+ output = ACTION_MAPPING[action](mod)
+ if output:
+ print '\n'.join(output)
+ if action not in ('adminindex', 'dbcheck', 'install', 'sqlindexes'):
+ print "COMMIT;"
diff --git a/django/bin/profiling/__init__.py b/django/bin/profiling/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/bin/profiling/__init__.py
diff --git a/django/bin/profiling/gather_profile_stats.py b/django/bin/profiling/gather_profile_stats.py
new file mode 100644
index 0000000000..852f16229d
--- /dev/null
+++ b/django/bin/profiling/gather_profile_stats.py
@@ -0,0 +1,34 @@
+"""
+gather_profile_stats.py /path/to/dir/of/profiles
+
+Note that the aggregated profiles must be read with pstats.Stats, not
+hotshot.stats (the formats are incompatible)
+"""
+
+from hotshot import stats
+import pstats
+import sys, os
+
+def gather_stats(p):
+ profiles = {}
+ for f in os.listdir(p):
+ if f.endswith('.agg.prof'):
+ path = f[:-9]
+ prof = pstats.Stats(os.path.join(p, f))
+ elif f.endswith('.prof'):
+ bits = f.split('.')
+ path = ".".join(bits[:-3])
+ prof = stats.load(os.path.join(p, f))
+ else:
+ continue
+ print "Processing %s" % f
+ if profiles.has_key(path):
+ profiles[path].add(prof)
+ else:
+ profiles[path] = prof
+ os.unlink(os.path.join(p, f))
+ for (path, prof) in profiles.items():
+ prof.dump_stats(os.path.join(p, "%s.agg.prof" % path))
+
+if __name__ == '__main__':
+ gather_stats(sys.argv[1])
diff --git a/django/bin/profiling/handler.py b/django/bin/profiling/handler.py
new file mode 100644
index 0000000000..8a7512b079
--- /dev/null
+++ b/django/bin/profiling/handler.py
@@ -0,0 +1,22 @@
+import hotshot, time, os
+from django.core.handler import CoreHandler
+
+PROFILE_DATA_DIR = "/var/log/cmsprofile/"
+
+def handler(req):
+ '''
+ Handler that uses hotshot to store profile data.
+
+ Stores profile data in PROFILE_DATA_DIR. Since hotshot has no way (that I
+ know of) to append profile data to a single file, each request gets its own
+ profile. The file names are in the format <url>.<n>.prof where <url> is
+ the request path with "/" replaced by ".", and <n> is a timestamp with
+ microseconds to prevent overwriting files.
+
+ Use the gather_profile_stats.py script to gather these individual request
+ profiles into aggregated profiles by request path.
+ '''
+ profname = "%s.%.3f.prof" % (req.uri.strip("/").replace('/', '.'), time.time())
+ profname = os.path.join(PROFILE_DATA_DIR, profname)
+ prof = hotshot.Profile(profname)
+ return prof.runcall(CoreHandler(), req)
diff --git a/django/bin/setup.py b/django/bin/setup.py
new file mode 100644
index 0000000000..086be541a0
--- /dev/null
+++ b/django/bin/setup.py
@@ -0,0 +1,45 @@
+"""
+Usage:
+
+python setup.py bdist
+python setup.py sdist
+"""
+
+from distutils.core import setup
+import os
+
+# Whether to include the .py files, rather than just .pyc's. Doesn't do anything yet.
+INCLUDE_SOURCE = True
+
+# Determines which apps are bundled with the distribution.
+INSTALLED_APPS = ('auth', 'categories', 'comments', 'core', 'media', 'news', 'polls', 'registration', 'search', 'sms', 'staff')
+
+# First, lump together all the generic, core packages that need to be included.
+packages = [
+ 'django',
+ 'django.core',
+ 'django.templatetags',
+ 'django.utils',
+ 'django.views',
+]
+for a in INSTALLED_APPS:
+ for dirname in ('parts', 'templatetags', 'views'):
+ if os.path.exists('django/%s/%s/' % (dirname, a)):
+ packages.append('django.%s.%s' % (dirname, a))
+
+# Next, add individual modules.
+py_modules = [
+ 'django.cron.daily_cleanup',
+ 'django.cron.search_indexer',
+]
+py_modules += ['django.models.%s' % a for a in INSTALLED_APPS]
+
+setup(
+ name = 'django',
+ version = '1.0',
+ packages = packages,
+ py_modules = py_modules,
+ url = 'http://www.ljworld.com/',
+ author = 'World Online',
+ author_email = 'cms-support@ljworld.com',
+)
diff --git a/django/bin/validate.py b/django/bin/validate.py
new file mode 100644
index 0000000000..f0c37d01cb
--- /dev/null
+++ b/django/bin/validate.py
@@ -0,0 +1,36 @@
+from django.core import meta
+
+def validate_app(app_label):
+ mod = meta.get_app(app_label)
+ for klass in mod._MODELS:
+ try:
+ validate_class(klass)
+ except AssertionError, e:
+ print e
+
+def validate_class(klass):
+ opts = klass._meta
+ # Fields.
+ for f in opts.fields:
+ if isinstance(f, meta.ManyToManyField):
+ assert isinstance(f.rel, meta.ManyToMany), "ManyToManyField %s should have 'rel' set to a ManyToMany instance." % f.name
+ # Inline related objects.
+ for rel_opts, rel_field in opts.get_inline_related_objects():
+ assert len([f for f in rel_opts.fields if f.core]) > 0, "At least one field in %s should have core=True, because it's being edited inline by %s." % (rel_opts.object_name, opts.object_name)
+ # All related objects.
+ related_apps_seen = []
+ for rel_opts, rel_field in opts.get_all_related_objects():
+ if rel_opts in related_apps_seen:
+ assert rel_field.rel.related_name is not None, "Relationship in field %s.%s needs to set 'related_name' because more than one %s object is referenced in %s." % (rel_opts.object_name, rel_field.name, opts.object_name, rel_opts.object_name)
+ related_apps_seen.append(rel_opts)
+ # Etc.
+ if opts.admin is not None:
+ assert opts.admin.ordering or opts.ordering, "%s needs to set 'ordering' on either its 'admin' or its model, because it has 'admin' set." % opts.object_name
+
+if __name__ == "__main__":
+ import sys
+ try:
+ validate_app(sys.argv[1])
+ except IndexError:
+ sys.stderr.write("Usage: %s [appname]\n" % __file__)
+ sys.exit(1)
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/__init__.py
diff --git a/django/conf/app_template/__init__.py b/django/conf/app_template/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/app_template/__init__.py
diff --git a/django/conf/app_template/models/__init__.py b/django/conf/app_template/models/__init__.py
new file mode 100644
index 0000000000..502a7d0738
--- /dev/null
+++ b/django/conf/app_template/models/__init__.py
@@ -0,0 +1 @@
+__all__ = ['{{ app_name }}']
diff --git a/django/conf/app_template/models/app_name.py b/django/conf/app_template/models/app_name.py
new file mode 100644
index 0000000000..6fce302e01
--- /dev/null
+++ b/django/conf/app_template/models/app_name.py
@@ -0,0 +1,3 @@
+from django.core import meta
+
+# Create your models here.
diff --git a/django/conf/app_template/urls/__init__.py b/django/conf/app_template/urls/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/app_template/urls/__init__.py
diff --git a/django/conf/app_template/urls/app_name.py b/django/conf/app_template/urls/app_name.py
new file mode 100644
index 0000000000..de814a56d1
--- /dev/null
+++ b/django/conf/app_template/urls/app_name.py
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('{{ project_name }}.apps.{{ app_name }}.views',
+# (r'', ''),
+)
diff --git a/django/conf/app_template/views/__init__.py b/django/conf/app_template/views/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/app_template/views/__init__.py
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
new file mode 100644
index 0000000000..871ef95a93
--- /dev/null
+++ b/django/conf/global_settings.py
@@ -0,0 +1,199 @@
+# Default Django settings. Override these with settings in the module
+# pointed-to by the DJANGO_SETTINGS_MODULE environment variable.
+
+import re
+
+####################
+# CORE #
+####################
+
+DEBUG = False
+
+# Whether to use the "Etag" header. This saves bandwidth but slows down performance.
+USE_ETAGS = False
+
+# people who get code error notifications
+ADMINS = (('Adrian Holovaty','aholovaty@ljworld.com'), ('Jacob Kaplan-Moss', 'jacob@lawrence.com'))
+
+# These IP addresses:
+# * See debug comments, when DEBUG is true
+# * Receive x-headers
+INTERNAL_IPS = (
+ '24.124.4.220', # World Online offices
+ '24.124.1.4', # https://admin.6newslawrence.com/
+ '24.148.30.138', # Adrian home
+ '127.0.0.1', # localhost
+)
+
+# Local time zone for this installation. All choices can be found here:
+# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'en-us'
+
+# Not-necessarily-technical managers of the site. They get broken link
+# notifications and other various e-mails.
+MANAGERS = ADMINS
+
+# which e-mail address error messages come from
+SERVER_EMAIL = None
+
+# Whether to send broken-link e-mails
+SEND_BROKEN_LINK_EMAILS = True
+
+# postgres database connection info
+DATABASE_ENGINE = 'postgresql'
+DATABASE_NAME = 'cms'
+DATABASE_USER = 'apache'
+DATABASE_PASSWORD = ''
+DATABASE_HOST = '' # set to empty string for localhost
+
+# host for sending e-mail
+EMAIL_HOST = 'localhost'
+
+# name of the session cookie
+AUTH_SESSION_COOKIE = 'rizzo'
+
+# name of the authorization profile module (below django.apps)
+AUTH_PROFILE_MODULE = ''
+
+# list of locations of the template source files, in search order
+TEMPLATE_DIRS = []
+
+# default e-mail address to use for various automated correspondence from the site managers
+DEFAULT_FROM_EMAIL = 'webmaster@ljworld.com'
+
+# whether to append trailing slashes to URLs
+APPEND_SLASH = True
+
+# whether to prepend the "www." subdomain to URLs
+PREPEND_WWW = False
+
+# list of regular expressions representing User-Agent strings that are not
+# allowed to visit any page, CMS-wide. Use this for bad robots/crawlers.
+DISALLOWED_USER_AGENTS = (
+ re.compile(r'^NaverBot.*'),
+ re.compile(r'^EmailSiphon.*'),
+ re.compile(r'^SiteSucker.*'),
+ re.compile(r'^sohu-search')
+)
+
+ABSOLUTE_URL_OVERRIDES = {}
+
+# list of allowed prefixes for the {% ssi %} tag
+ALLOWED_INCLUDE_ROOTS = ('/home/html',)
+
+# if this is a admin settings module, this should be a list of
+# settings modules for which this admin is an admin for
+ADMIN_FOR = []
+
+# 404s that may be ignored
+IGNORABLE_404_STARTS = ('/cgi-bin/', '/_vti_bin', '/_vti_inf')
+IGNORABLE_404_ENDS = ('mail.pl', 'mailform.pl', 'mail.cgi', 'mailform.cgi', 'favicon.ico', '.php')
+
+##############
+# Middleware #
+##############
+
+# List of middleware classes to use. Order is important; in the request phase,
+# 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.common.CommonMiddleware",
+ "django.middleware.doc.XViewMiddleware",
+)
+
+#########
+# CACHE #
+#########
+
+# The cache backend to use. See the docstring in django.core.cache for the
+# values this can be set to.
+CACHE_BACKEND = 'simple://'
+
+####################
+# REGISTRATION #
+####################
+
+# E-mail addresses at these domains cannot sign up for accounts
+BANNED_EMAIL_DOMAINS = [
+ 'mailinator.com', 'dodgeit.com', 'spamgourmet.com', 'mytrashmail.com'
+]
+REGISTRATION_COOKIE_DOMAIN = None # set to a string like ".lawrence.com", or None for standard domain cookie
+
+# If this is set to True, users will be required to fill out their profile
+# (defined by AUTH_PROFILE_MODULE) before they will be allowed to create
+# an account.
+REGISTRATION_REQUIRES_PROFILE = False
+
+####################
+# COMMENTS #
+####################
+
+COMMENTS_ALLOW_PROFANITIES = False
+
+# The group ID that designates which users are banned.
+# Set to None if you're not using it.
+COMMENTS_BANNED_USERS_GROUP = 19
+
+# The group ID that designates which users can moderate comments.
+# Set to None if you're not using it.
+COMMENTS_MODERATORS_GROUP = 20
+
+# The group ID that designates the users whose comments should be e-mailed to MANAGERS.
+# Set to None if you're not using it.
+COMMENTS_SKETCHY_USERS_GROUP = 22
+
+# The system will e-mail MANAGERS the first COMMENTS_FIRST_FEW comments by each
+# user. Set this to 0 if you want to disable it.
+COMMENTS_FIRST_FEW = 10
+
+BANNED_IPS = (
+ # Dupont Stainmaster / GuessWho / a variety of other names (back when we had free comments)
+ '204.94.104.99', '66.142.59.23', '220.196.165.142',
+ # (Unknown)
+ '64.65.191.117',
+# # Jimmy_Olsen / Clark_Kent / Bruce_Wayne
+# # Unbanned on 2005-06-17, because other people want to register from this address.
+# '12.106.111.10',
+ # hoof_hearted / hugh_Jass / Ferd_Burfel / fanny_farkel
+ '24.124.72.20', '170.135.241.46',
+ # Zac_McGraw
+ '198.74.20.74', '198.74.20.75',
+)
+
+####################
+# BLOGS #
+####################
+
+# E-mail addresses to notify when a new blog entry is posted live
+BLOGS_EMAILS_TO_NOTIFY = []
+
+####################
+# PLACES #
+####################
+
+# A list of IDs -- *as integers, not strings* -- that are considered the "main"
+# cities served by this installation. Probably just one.
+MAIN_CITY_IDS = (1,) # Lawrence
+
+# A list of IDs -- *as integers, not strings* -- that are considered "local" by
+# this installation.
+LOCAL_CITY_IDS = (1, 3) # Lawrence and Kansas City, MO
+
+####################
+# THUMBNAILS #
+####################
+
+THUMB_ALLOWED_WIDTHS = (90, 120, 180, 240, 450)
+
+####################
+# VARIOUS ROOTS #
+####################
+
+# This is the new media root and URL! Use it, and only it!
+MEDIA_ROOT = '/home/media/media.lawrence.com/'
+MEDIA_URL = 'http://media.lawrence.com'
diff --git a/django/conf/project_template/__init__.py b/django/conf/project_template/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/project_template/__init__.py
diff --git a/django/conf/project_template/apps/__init__.py b/django/conf/project_template/apps/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/project_template/apps/__init__.py
diff --git a/django/conf/project_template/settings/__init__.py b/django/conf/project_template/settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/project_template/settings/__init__.py
diff --git a/django/conf/project_template/settings/main.py b/django/conf/project_template/settings/main.py
new file mode 100644
index 0000000000..3db0e7961c
--- /dev/null
+++ b/django/conf/project_template/settings/main.py
@@ -0,0 +1,31 @@
+# Django settings for {{ app_name }} project.
+
+DEBUG = False
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+LANGUAGE_CODE = 'en-us'
+
+DATABASE_ENGINE = 'postgresql' # Either 'postgresql' or 'mysql'.
+DATABASE_NAME = ''
+DATABASE_USER = ''
+DATABASE_HOST = '' # Set to empty string for localhost.
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT.
+# Example: "http://media.lawrence.com"
+MEDIA_URL = ''
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates".
+)
+
+INSTALLED_APPS = (
+)
diff --git a/django/conf/settings.py b/django/conf/settings.py
new file mode 100644
index 0000000000..dda46dba2f
--- /dev/null
+++ b/django/conf/settings.py
@@ -0,0 +1,42 @@
+"""
+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
+
+# 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:
+ mod = __import__(os.environ['DJANGO_SETTINGS_MODULE'], '', '', [''])
+except (KeyError, ImportError, ValueError):
+ pass
+else:
+ for setting in dir(mod):
+ if setting == setting.upper():
+ setattr(me, setting, getattr(mod, setting))
+
+# save DJANGO_SETTINGS_MODULE in case anyone in the future cares
+me.SETTINGS_MODULE = os.environ.get('DJANGO_SETTINGS_MODULE', '')
+
+# 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
+
diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/conf/urls/__init__.py
diff --git a/django/conf/urls/admin.py b/django/conf/urls/admin.py
new file mode 100644
index 0000000000..92d5d1bc9e
--- /dev/null
+++ b/django/conf/urls/admin.py
@@ -0,0 +1,56 @@
+from django.conf.urls.defaults import *
+from django.conf.settings import INSTALLED_APPS
+
+urlpatterns = (
+ ('^/?$', 'django.views.admin.main.index'),
+ ('^logout/$', 'django.views.admin.main.logout'),
+ ('^password_change/$', 'django.views.registration.passwords.password_change'),
+ ('^password_change/done/$', 'django.views.registration.passwords.password_change_done'),
+ ('^template_validator/$', 'django.views.admin.template.template_validator'),
+
+ # Documentation
+ ('^doc/$', 'django.views.admin.doc.doc_index'),
+ ('^doc/bookmarklets/$', 'django.views.admin.doc.bookmarklets'),
+ ('^doc/tags/$', 'django.views.admin.doc.template_tag_index'),
+ ('^doc/filters/$', 'django.views.admin.doc.template_filter_index'),
+ ('^doc/views/$', 'django.views.admin.doc.view_index'),
+ ('^doc/views/jump/$', 'django.views.admin.doc.jump_to_view'),
+ ('^doc/views/(?P<view>[^/]+)/$', 'django.views.admin.doc.view_detail'),
+ ('^doc/models/$', 'django.views.admin.doc.model_index'),
+ ('^doc/models/(?P<model>[^/]+)/$', 'django.views.admin.doc.model_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.views.admin.main.change_list'),
+ ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/add/$', 'django.views.admin.main.add_stage'),
+ ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/(?P<object_id>\d+)/$', 'django.views.admin.main.change_stage'),
+ ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/(?P<object_id>\d+)/delete/$', 'django.views.admin.main.delete_stage'),
+ ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/(?P<object_id>\d+)/history/$', 'django.views.admin.main.history'),
+ ('^(?P<app_label>[^/]+)/(?P<module_name>[^/]+)/jsvalidation/$', 'django.views.admin.jsvalidation.jsvalidation'),
+)
+urlpatterns = patterns('', *urlpatterns)
diff --git a/django/conf/urls/admin_password_reset.py b/django/conf/urls/admin_password_reset.py
new file mode 100644
index 0000000000..4a8b166f61
--- /dev/null
+++ b/django/conf/urls/admin_password_reset.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django.views',
+ (r'^/?$', 'registration.passwords.password_reset', {'is_admin_site' : True}),
+ (r'^done/$', 'registration.passwords.password_reset_done'),
+)
diff --git a/django/conf/urls/comments.py b/django/conf/urls/comments.py
new file mode 100644
index 0000000000..f117e8924a
--- /dev/null
+++ b/django/conf/urls/comments.py
@@ -0,0 +1,12 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django.views',
+ (r'^post/$', 'comments.comments.post_comment'),
+ (r'^postfree/$', 'comments.comments.post_free_comment'),
+ (r'^posted/$', 'comments.comments.comment_was_posted'),
+ (r'^karma/vote/(?P<comment_id>\d+)/(?P<vote>up|down)/$', 'comments.karma.vote'),
+ (r'^flag/(?P<comment_id>\d+)/$', 'comments.userflags.flag'),
+ (r'^flag/(?P<comment_id>\d+)/done/$', 'comments.userflags.flag_done'),
+ (r'^delete/(?P<comment_id>\d+)/$', 'comments.userflags.delete'),
+ (r'^delete/(?P<comment_id>\d+)/done/$', 'comments.userflags.delete_done'),
+)
diff --git a/django/conf/urls/defaults.py b/django/conf/urls/defaults.py
new file mode 100644
index 0000000000..9971dd2eb7
--- /dev/null
+++ b/django/conf/urls/defaults.py
@@ -0,0 +1,17 @@
+from django.core.urlresolvers import RegexURLMultiplePattern, RegexURLPattern
+
+__all__ = ['handler404', 'handler500', 'include', 'patterns']
+
+handler404 = 'django.views.defaults.page_not_found'
+handler500 = 'django.views.defaults.server_error'
+
+include = lambda urlconf_module: [urlconf_module]
+
+def patterns(prefix, *tuples):
+ pattern_list = []
+ for t in tuples:
+ if type(t[1]) == list:
+ pattern_list.append(RegexURLMultiplePattern(t[0], t[1][0]))
+ else:
+ pattern_list.append(RegexURLPattern(t[0], prefix and (prefix + '.' + t[1]) or t[1], *t[2:]))
+ return pattern_list
diff --git a/django/conf/urls/flatfiles.py b/django/conf/urls/flatfiles.py
new file mode 100644
index 0000000000..1d29463319
--- /dev/null
+++ b/django/conf/urls/flatfiles.py
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django.views',
+ (r'^(?P<url>.*)$', 'core.flatfiles.flat_file'),
+)
diff --git a/django/conf/urls/registration.py b/django/conf/urls/registration.py
new file mode 100644
index 0000000000..5a56fe5e05
--- /dev/null
+++ b/django/conf/urls/registration.py
@@ -0,0 +1,19 @@
+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'^register/$', 'ellington.registration.views.registration.signup'),
+ (r'^register/(?P<challenge_string>\w{32})/$', 'ellington.registration.views.registration.register_form'),
+
+ (r'^profile/$', 'ellington.registration.views.profile.profile'),
+ (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'),
+)
diff --git a/django/conf/urls/rss.py b/django/conf/urls/rss.py
new file mode 100644
index 0000000000..af26f7cdb5
--- /dev/null
+++ b/django/conf/urls/rss.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django.views',
+ (r'^(?P<slug>\w+)/$', 'rss.rss.feed'),
+ (r'^(?P<slug>\w+)/(?P<param>[\w/]+)/$', 'rss.rss.feed'),
+)
diff --git a/django/conf/urls/shortcut.py b/django/conf/urls/shortcut.py
new file mode 100644
index 0000000000..f0ed9d9fd0
--- /dev/null
+++ b/django/conf/urls/shortcut.py
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('django.views',
+ (r'^(?P<content_type_id>\d+)/(?P<object_id>\d+)/$', 'defaults.shortcut'),
+)
diff --git a/django/core/__init__.py b/django/core/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/core/__init__.py
diff --git a/django/core/cache.py b/django/core/cache.py
new file mode 100644
index 0000000000..a133874672
--- /dev/null
+++ b/django/core/cache.py
@@ -0,0 +1,255 @@
+"""
+Caching framework.
+
+This module defines set of cache backends that all conform to a simple API.
+In a nutshell, a cache is a set of values -- which can be any object that
+may be pickled -- identified by string keys. For the complete API, see
+the abstract Cache object, below.
+
+Client code should not access a cache backend directly; instead
+it should use the get_cache() function. This function will look at
+settings.CACHE_BACKEND and use that to create and load a cache object.
+
+The CACHE_BACKEND setting is a quasi-URI; examples are:
+
+ memcached://127.0.0.1:11211/ A memcached backend; the server is running
+ on localhost port 11211.
+
+ pgsql://tablename/ A pgsql backend (the pgsql backend uses
+ the same database/username as the rest of
+ the CMS, so only a table name is needed.)
+
+ file:///var/tmp/django.cache/ A file-based cache at /var/tmp/django.cache
+
+ simple:/// A simple single-process memory cache; you
+ probably don't want to use this except for
+ testing. Note that this cache backend is
+ NOT threadsafe!
+
+All caches may take arguments; these are given in query-string style. Valid
+arguments are:
+
+ timeout
+ Default timeout, in seconds, to use for the cache. Defaults
+ to 5 minutes (300 seconds).
+
+ max_entries
+ For the simple, file, and database backends, the maximum number of
+ entries allowed in the cache before it is cleaned. Defaults to
+ 300.
+
+ cull_percentage
+ The percentage of entries that are culled when max_entries is reached.
+ The actual percentage is 1/cull_percentage, so set cull_percentage=3 to
+ cull 1/3 of the entries when max_entries is reached.
+
+ A value of 0 for cull_percentage means that the entire cache will be
+ dumped when max_entries is reached. This makes culling *much* faster
+ at the expense of more cache misses.
+
+For example:
+
+ memcached://127.0.0.1:11211/?timeout=60
+ pgsql://tablename/?timeout=120&max_entries=500&cull_percentage=4
+
+Invalid arguments are silently ignored, as are invalid values of known
+arguments.
+
+So far, only the memcached and simple backend have been implemented; backends
+using postgres, and file-system storage are planned.
+"""
+
+##############
+# Exceptions #
+##############
+
+class InvalidCacheBackendError(Exception):
+ pass
+
+################################
+# Abstract base implementation #
+################################
+
+class _Cache:
+
+ def __init__(self, params):
+ timeout = params.get('timeout', 300)
+ try:
+ timeout = int(timeout)
+ except (ValueError, TypeError):
+ timeout = 300
+ self.default_timeout = timeout
+
+ def get(self, key, default=None):
+ '''
+ Fetch a given key from the cache. If the key does not exist, return
+ default, which itself defaults to None.
+ '''
+ raise NotImplementedError
+
+ def set(self, key, value, timeout=None):
+ '''
+ Set a value in the cache. If timeout is given, that timeout will be
+ used for the key; otherwise the default cache timeout will be used.
+ '''
+ raise NotImplementedError
+
+ def delete(self, key):
+ '''
+ Delete a key from the cache, failing silently.
+ '''
+ raise NotImplementedError
+
+ def get_many(self, keys):
+ '''
+ Fetch a bunch of keys from the cache. For certain backends (memcached,
+ pgsql) this can be *much* faster when fetching multiple values.
+
+ Returns a dict mapping each key in keys to its value. If the given
+ key is missing, it will be missing from the response dict.
+ '''
+ d = {}
+ for k in keys:
+ val = self.get(k)
+ if val is not None:
+ d[k] = val
+ return d
+
+ def has_key(self, key):
+ '''
+ Returns True if the key is in the cache and has not expired.
+ '''
+ return self.get(key) is not None
+
+###########################
+# memcached cache backend #
+###########################
+
+try:
+ import memcache
+except ImportError:
+ _MemcachedCache = None
+else:
+ class _MemcachedCache(_Cache):
+ """Memcached cache backend."""
+
+ def __init__(self, server, params):
+ _Cache.__init__(self, params)
+ self._cache = memcache.Client([server])
+
+ def get(self, key, default=None):
+ val = self._cache.get(key)
+ if val is None:
+ return default
+ else:
+ return val
+
+ def set(self, key, value, timeout=0):
+ self._cache.set(key, value, timeout)
+
+ def delete(self, key):
+ self._cache.delete(key)
+
+ def get_many(self, keys):
+ return self._cache.get_multi(keys)
+
+##################################
+# Single-process in-memory cache #
+##################################
+
+import time
+
+class _SimpleCache(_Cache):
+ """Simple single-process in-memory cache"""
+
+ def __init__(self, host, params):
+ _Cache.__init__(self, params)
+ self._cache = {}
+ self._expire_info = {}
+
+ max_entries = params.get('max_entries', 300)
+ try:
+ self._max_entries = int(max_entries)
+ except (ValueError, TypeError):
+ self._max_entries = 300
+
+ cull_frequency = params.get('cull_frequency', 3)
+ try:
+ self._cull_frequency = int(cull_frequency)
+ except (ValueError, TypeError):
+ self._cull_frequency = 3
+
+ def get(self, key, default=None):
+ now = time.time()
+ exp = self._expire_info.get(key, now)
+ if exp is not None and exp < now:
+ del self._cache[key]
+ del self._expire_info[key]
+ return default
+ else:
+ return self._cache.get(key, default)
+
+ def set(self, key, value, timeout=None):
+ if len(self._cache) >= self._max_entries:
+ self._cull()
+ if timeout is None:
+ timeout = self.default_timeout
+ self._cache[key] = value
+ self._expire_info[key] = time.time() + timeout
+
+ def delete(self, key):
+ try:
+ del self._cache[key]
+ except KeyError:
+ pass
+ try:
+ del self._expire_info[key]
+ except KeyError:
+ pass
+
+ def has_key(self, key):
+ return self._cache.has_key(key)
+
+ def _cull(self):
+ if self._cull_frequency == 0:
+ self._cache.clear()
+ self._expire_info.clear()
+ else:
+ doomed = [k for (i, k) in enumerate(self._cache) if i % self._cull_frequency == 0]
+ for k in doomed:
+ self.delete(k)
+
+##########################################
+# Read settings and load a cache backend #
+##########################################
+
+from cgi import parse_qsl
+
+_BACKENDS = {
+ 'memcached' : _MemcachedCache,
+ 'simple' : _SimpleCache,
+}
+
+def get_cache(backend_uri):
+ if backend_uri.find(':') == -1:
+ raise InvalidCacheBackendError("Backend URI must start with scheme://")
+ scheme, rest = backend_uri.split(':', 1)
+ if not rest.startswith('//'):
+ raise InvalidCacheBackendError("Backend URI must start with scheme://")
+ if scheme not in _BACKENDS.keys():
+ raise InvalidCacheBackendError("%r is not a valid cache backend" % scheme)
+
+ host = rest[2:]
+ qpos = rest.find('?')
+ if qpos != -1:
+ params = dict(parse_qsl(rest[qpos+1:]))
+ host = rest[:qpos]
+ else:
+ params = {}
+ if host.endswith('/'):
+ host = host[:-1]
+
+ return _BACKENDS[scheme](host, params)
+
+from django.conf.settings import CACHE_BACKEND
+cache = get_cache(CACHE_BACKEND)
diff --git a/django/core/db/__init__.py b/django/core/db/__init__.py
new file mode 100644
index 0000000000..deede3d7ca
--- /dev/null
+++ b/django/core/db/__init__.py
@@ -0,0 +1,28 @@
+"""
+This is the core database connection.
+
+All CMS 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
+
+Right now, we're handling this by using psycopg's custom typecast definitions.
+If we move to a different database module, we should ensure that it either
+performs the appropriate typecasting out of the box, or that it has hooks that
+let us do that.
+"""
+
+from django.conf.settings import DATABASE_ENGINE
+
+dbmod = __import__('django.core.db.backends.%s' % DATABASE_ENGINE, '', '', [''])
+
+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
+OPERATOR_MAPPING = dbmod.OPERATOR_MAPPING
+DATA_TYPES = dbmod.DATA_TYPES
diff --git a/django/core/db/backends/__init__.py b/django/core/db/backends/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/core/db/backends/__init__.py
diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py
new file mode 100644
index 0000000000..4585600ce4
--- /dev/null
+++ b/django/core/db/backends/mysql.py
@@ -0,0 +1,107 @@
+"""
+MySQL database backend for Django.
+
+Requires MySQLdb: http://sourceforge.net/projects/mysql-python
+"""
+
+from django.core.db import base, typecasts
+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,
+})
+
+class DatabaseWrapper:
+ def __init__(self):
+ self.connection = None
+ self.queries = []
+
+ def cursor(self):
+ from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG
+ if self.connection is None:
+ self.connection = Database.connect(user=DATABASE_USER, db=DATABASE_NAME,
+ passwd=DATABASE_PASSWORD, host=DATABASE_HOST, conv=django_conversions)
+ if DEBUG:
+ return base.CursorDebugWrapper(self.connection.cursor(), self)
+ return self.connection.cursor()
+
+ def commit(self):
+ pass
+
+ def rollback(self):
+ pass
+
+ def close(self):
+ if self.connection is not None:
+ self.connection.close()
+ self.connection = None
+
+def dictfetchone(cursor):
+ "Returns a row from the cursor as a dict"
+ raise NotImplementedError
+
+def dictfetchmany(cursor, number):
+ "Returns a certain number of rows from a cursor as a dict"
+ raise NotImplementedError
+
+def dictfetchall(cursor):
+ "Returns all rows from a cursor as a dict"
+ raise NotImplementedError
+
+def get_last_insert_id(cursor, table_name, pk_name):
+ cursor.execute("SELECT LAST_INSERT_ID()")
+ return cursor.fetchone()[0]
+
+OPERATOR_MAPPING = {
+ 'exact': '=',
+ 'iexact': 'LIKE',
+ 'contains': 'LIKE',
+ 'icontains': 'LIKE',
+ 'ne': '!=',
+ 'gt': '>',
+ 'gte': '>=',
+ 'lt': '<',
+ 'lte': '<=',
+ 'startswith': 'LIKE',
+ 'endswith': 'LIKE'
+}
+
+# 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': 'mediumint(9) auto_increment',
+ 'BooleanField': 'bool',
+ 'CharField': 'varchar(%(maxlength)s)',
+ 'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
+ 'DateField': 'date',
+ 'DateTimeField': 'datetime',
+ 'EmailField': 'varchar(75)',
+ 'FileField': 'varchar(100)',
+ 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
+ 'ImageField': 'varchar(100)',
+ 'IntegerField': 'integer',
+ 'IPAddressField': 'char(15)',
+ 'ManyToManyField': None,
+ 'NullBooleanField': 'bool',
+ 'PhoneNumberField': 'varchar(20)',
+ 'PositiveIntegerField': 'integer UNSIGNED',
+ 'PositiveSmallIntegerField': 'smallint UNSIGNED',
+ 'SlugField': 'varchar(50)',
+ 'SmallIntegerField': 'smallint',
+ 'TextField': 'text',
+ 'TimeField': 'time',
+ 'URLField': 'varchar(200)',
+ 'USStateField': 'varchar(2)',
+ 'XMLField': 'text',
+}
diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py
new file mode 100644
index 0000000000..44cb0da256
--- /dev/null
+++ b/django/core/db/backends/postgresql.py
@@ -0,0 +1,109 @@
+"""
+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
+
+class DatabaseWrapper:
+ def __init__(self):
+ self.connection = None
+ self.queries = []
+
+ def cursor(self):
+ from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG, TIME_ZONE
+ if self.connection is None:
+ # Note that "host=" has to be last, because it might be blank.
+ self.connection = Database.connect("user=%s dbname=%s password=%s host=%s" % \
+ (DATABASE_USER, DATABASE_NAME, DATABASE_PASSWORD, DATABASE_HOST))
+ 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 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]
+
+# 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.
+Database.register_type(Database.new_type((1082,), "DATE", typecasts.typecast_date))
+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': '=',
+ 'iexact': 'ILIKE',
+ 'contains': 'LIKE',
+ 'icontains': 'ILIKE',
+ 'ne': '!=',
+ 'gt': '>',
+ 'gte': '>=',
+ 'lt': '<',
+ 'lte': '<=',
+ 'startswith': 'LIKE',
+ 'endswith': 'LIKE'
+}
+
+# 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',
+ 'EmailField': 'varchar(75)',
+ 'FileField': 'varchar(100)',
+ 'FloatField': 'numeric(%(max_digits)s, %(decimal_places)s)',
+ 'ImageField': 'varchar(100)',
+ 'IntegerField': 'integer',
+ 'IPAddressField': 'inet',
+ 'ManyToManyField': None,
+ 'NullBooleanField': 'boolean',
+ 'PhoneNumberField': 'varchar(20)',
+ 'PositiveIntegerField': 'integer CHECK (%(name)s >= 0)',
+ 'PositiveSmallIntegerField': 'smallint CHECK (%(name)s >= 0)',
+ 'SlugField': 'varchar(50)',
+ 'SmallIntegerField': 'smallint',
+ 'TextField': 'text',
+ 'TimeField': 'time',
+ 'URLField': 'varchar(200)',
+ 'USStateField': 'varchar(2)',
+ 'XMLField': 'text',
+}
diff --git a/django/core/db/base.py b/django/core/db/base.py
new file mode 100644
index 0000000000..62cca9d0be
--- /dev/null
+++ b/django/core/db/base.py
@@ -0,0 +1,32 @@
+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/typecasts.py b/django/core/db/typecasts.py
new file mode 100644
index 0000000000..1ede949aad
--- /dev/null
+++ b/django/core/db/typecasts.py
@@ -0,0 +1,42 @@
+import datetime
+
+###############################################
+# Converters from database (string) to Python #
+###############################################
+
+def typecast_date(s):
+ return s and datetime.date(*map(int, s.split('-'))) # returns None if s is null
+
+def typecast_time(s): # does NOT store time zone information
+ if not s: return None
+ bits = s.split(':')
+ if len(bits[2].split('.')) > 1: # if there is a decimal (e.g. '11:16:36.181305')
+ return datetime.time(int(bits[0]), int(bits[1]), int(bits[2].split('.')[0]),
+ int(bits[2].split('.')[1].split('-')[0]))
+ else: # no decimal was found (e.g. '12:30:00')
+ return datetime.time(int(bits[0]), int(bits[1]), int(bits[2].split('.')[0]), 0)
+
+def typecast_timestamp(s): # does NOT store time zone information
+ if not s: return None
+ d, t = s.split()
+ 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.split('-')[0]),
+ int(microseconds.split('-')[0]))
+
+def typecast_boolean(s):
+ if s is None: return None
+ 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/defaultfilters.py b/django/core/defaultfilters.py
new file mode 100644
index 0000000000..1ccf07b32e
--- /dev/null
+++ b/django/core/defaultfilters.py
@@ -0,0 +1,466 @@
+"Default variable filters"
+
+import template, re, random
+
+###################
+# STRINGS #
+###################
+
+def addslashes(value, _):
+ "Adds slashes - useful for passing strings to JavaScript, for example."
+ return value.replace('"', '\\"').replace("'", "\\'")
+
+def capfirst(value, _):
+ "Capitalizes the first character of the value"
+ value = str(value)
+ return value and value[0].upper() + value[1:]
+
+def fix_ampersands(value, _):
+ "Replaces ampersands with ``&amp;`` entities"
+ from django.utils.html import fix_ampersands
+ return fix_ampersands(value)
+
+def floatformat(text, _):
+ """
+ Displays a floating point number as 34.2 (with one decimal places) - but
+ only if there's a point to be displayed
+ """
+ if not text:
+ return ''
+ if text - int(text) < 0.1:
+ return int(text)
+ return "%.1f" % text
+
+def linenumbers(value, _):
+ "Displays text with line numbers"
+ from django.utils.html import escape
+ lines = value.split('\n')
+ # Find the maximum width of the line count, for use with zero padding string format command
+ width = str(len(str(len(lines))))
+ for i, line in enumerate(lines):
+ lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line))
+ return '\n'.join(lines)
+
+def lower(value, _):
+ "Converts a string into all lowercase"
+ return value.lower()
+
+def make_list(value, _):
+ """
+ Returns the value turned into a list. For an integer, it's a list of
+ digits. For a string, it's a list of characters.
+ """
+ return list(str(value))
+
+def slugify(value, _):
+ "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
+ value = re.sub('[^\w\s]', '', value).strip().lower()
+ return re.sub('\s+', '-', value)
+
+def stringformat(value, arg):
+ """
+ Formats the variable according to the argument, a string formatting specifier.
+ This specifier uses Python string formating syntax, with the exception that
+ the leading "%" is dropped.
+
+ See http://docs.python.org/lib/typesseq-strings.html for documentation
+ of Python string formatting
+ """
+ try:
+ return ("%" + arg) % value
+ except (ValueError, TypeError):
+ return ""
+
+def title(value, _):
+ "Converts a string into titlecase"
+ return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
+
+def truncatewords(value, arg):
+ """
+ Truncates a string after a certain number of words
+
+ Argument: Number of words to truncate after
+ """
+ from django.utils.text import truncate_words
+ try:
+ length = int(arg)
+ except ValueError: # invalid literal for int()
+ return value # Fail silently.
+ if not isinstance(value, basestring):
+ value = str(value)
+ return truncate_words(value, length)
+
+def upper(value, _):
+ "Converts a string into all uppercase"
+ return value.upper()
+
+def urlencode(value, _):
+ "Escapes a value for use in a URL"
+ import urllib
+ return urllib.quote(value)
+
+def urlize(value, _):
+ "Converts URLs in plain text into clickable links"
+ from django.utils.html import urlize
+ return urlize(value, nofollow=True)
+
+def urlizetrunc(value, limit):
+ """
+ Converts URLs into clickable links, truncating URLs to the given character limit
+
+ Argument: Length to truncate URLs to.
+ """
+ from django.utils.html import urlize
+ return urlize(value, trim_url_limit=int(limit), nofollow=True)
+
+def wordcount(value, _):
+ "Returns the number of words"
+ return len(value.split())
+
+def wordwrap(value, arg):
+ """
+ Wraps words at specified line length
+
+ Argument: number of words to wrap the text at.
+ """
+ from django.utils.text import wrap
+ return wrap(value, int(arg))
+
+def ljust(value, arg):
+ """
+ Left-aligns the value in a field of a given width
+
+ Argument: field size
+ """
+ return str(value).ljust(int(arg))
+
+def rjust(value, arg):
+ """
+ Right-aligns the value in a field of a given width
+
+ Argument: field size
+ """
+ return str(value).rjust(int(arg))
+
+def center(value, arg):
+ "Centers the value in a field of a given width"
+ return str(value).center(int(arg))
+
+def cut(value, arg):
+ "Removes all values of arg from the given string"
+ return value.replace(arg, '')
+
+###################
+# HTML STRINGS #
+###################
+
+def escape(value, _):
+ "Escapes a string's HTML"
+ from django.utils.html import escape
+ return escape(value)
+
+def linebreaks(value, _):
+ "Converts newlines into <p> and <br />s"
+ from django.utils.html import linebreaks
+ return linebreaks(value)
+
+def linebreaksbr(value, _):
+ "Converts newlines into <br />s"
+ return value.replace('\n', '<br />')
+
+def removetags(value, tags):
+ "Removes a space separated list of [X]HTML tags from the output"
+ tags = [re.escape(tag) for tag in tags.split()]
+ tags_re = '(%s)' % '|'.join(tags)
+ starttag_re = re.compile('<%s(>|(\s+[^>]*>))' % tags_re)
+ endtag_re = re.compile('</%s>' % tags_re)
+ value = starttag_re.sub('', value)
+ value = endtag_re.sub('', value)
+ return value
+
+def striptags(value, _):
+ "Strips all [X]HTML tags"
+ from django.utils.html import strip_tags
+ if not isinstance(value, basestring):
+ value = str(value)
+ return strip_tags(value)
+
+###################
+# LISTS #
+###################
+
+def dictsort(value, arg):
+ """
+ Takes a list of dicts, returns that list sorted by the property given in
+ the argument.
+ """
+ decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value]
+ decorated.sort()
+ return [item[1] for item in decorated]
+
+def dictsortreversed(value, arg):
+ """
+ Takes a list of dicts, returns that list sorted in reverse order by the
+ property given in the argument.
+ """
+ decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value]
+ decorated.sort()
+ decorated.reverse()
+ return [item[1] for item in decorated]
+
+def first(value, _):
+ "Returns the first item in a list"
+ try:
+ return value[0]
+ except IndexError:
+ return ''
+
+def join(value, arg):
+ "Joins a list with a string, like Python's ``str.join(list)``"
+ try:
+ return arg.join(map(str, value))
+ except AttributeError: # fail silently but nicely
+ return value
+
+def length(value, _):
+ "Returns the length of the value - useful for lists"
+ return len(value)
+
+def length_is(value, arg):
+ "Returns a boolean of whether the value's length is the argument"
+ return len(value) == int(arg)
+
+def random(value, _):
+ "Returns a random item from the list"
+ return random.choice(value)
+
+def slice_(value, arg):
+ """
+ Returns a slice of the list.
+
+ Uses the same syntax as Python's list slicing; see
+ http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice
+ for an introduction.
+ """
+ try:
+ start, finish = arg.split(':')
+ except ValueError: # unpack list of wrong size
+ return value # fail silently but nicely
+ try:
+ if start and finish:
+ return value[int(start):int(finish)]
+ if start:
+ return value[int(start):]
+ if finish:
+ return value[:int(finish)]
+ except TypeError:
+ pass
+ return value
+
+def unordered_list(value, _):
+ """
+ Recursively takes a self-nested list and returns an HTML unordered list --
+ WITHOUT opening and closing <ul> tags.
+
+ The list is assumed to be in the proper format. For example, if ``var`` contains
+ ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``,
+ then ``{{ var|unordered_list }}`` would return::
+
+ <li>States
+ <ul>
+ <li>Kansas
+ <ul>
+ <li>Lawrence</li>
+ <li>Topeka</li>
+ </ul>
+ </li>
+ <li>Illinois</li>
+ </ul>
+ </li>
+ """
+ def _helper(value, tabs):
+ indent = '\t' * tabs
+ if value[1]:
+ return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, value[0], indent,
+ '\n'.join([unordered_list(v, tabs+1) for v in value[1]]), indent, indent)
+ else:
+ return '%s<li>%s</li>' % (indent, value[0])
+ return _helper(value, 1)
+
+###################
+# INTEGERS #
+###################
+
+def add(value, arg):
+ "Adds the arg to the value"
+ return int(value) + int(arg)
+
+def get_digit(value, arg):
+ """
+ Given a whole number, returns the requested digit of it, where 1 is the
+ right-most digit, 2 is the second-right-most digit, etc. Returns the
+ original value for invalid input (if input or argument is not an integer,
+ or if argument is less than 1). Otherwise, output is always an integer.
+ """
+ try:
+ arg = int(arg)
+ value = int(value)
+ except ValueError:
+ return value # Fail silently for an invalid argument
+ if arg < 1:
+ return value
+ try:
+ return int(str(value)[-arg])
+ except IndexError:
+ return 0
+
+###################
+# DATES #
+###################
+
+def date(value, arg):
+ "Formats a date according to the given format"
+ from django.utils.dateformat import format
+ return format(value, arg)
+
+def time(value, arg):
+ "Formats a time according to the given format"
+ from django.utils.dateformat import time_format
+ return time_format(value, arg)
+
+def timesince(value, _):
+ 'Formats a date as the time since that date (i.e. "4 days, 6 hours")'
+ from django.utils.timesince import timesince
+ return timesince(value)
+
+###################
+# LOGIC #
+###################
+
+def default(value, arg):
+ "If value is unavailable, use given default"
+ return value or arg
+
+def divisibleby(value, arg):
+ "Returns true if the value is devisible by the argument"
+ return int(value) % int(arg) == 0
+
+def yesno(value, arg):
+ """
+ Given a string mapping values for true, false and (optionally) None,
+ returns one of those strings accoding to the value:
+
+ ========== ====================== ==================================
+ Value Argument Outputs
+ ========== ====================== ==================================
+ ``True`` ``"yeah,no,maybe"`` ``yeah``
+ ``False`` ``"yeah,no,maybe"`` ``no``
+ ``None`` ``"yeah,no,maybe"`` ``maybe``
+ ``None`` ``"yeah,no"`` ``"no"`` (converts None to False
+ if no mapping for None is given.
+ ========== ====================== ==================================
+ """
+ bits = arg.split(',')
+ if len(bits) < 2:
+ return value # Invalid arg.
+ try:
+ yes, no, maybe = bits
+ except ValueError: # unpack list of wrong size (no "maybe" value provided)
+ yes, no, maybe = bits, bits[1]
+ if value is None:
+ return maybe
+ if value:
+ return yes
+ return no
+
+###################
+# MISC #
+###################
+
+def filesizeformat(bytes, _):
+ """
+ Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102
+ bytes, etc).
+ """
+ bytes = float(bytes)
+ if bytes < 1024:
+ return "%d byte%s" % (bytes, bytes != 1 and 's' or '')
+ if bytes < 1024 * 1024:
+ return "%.1f KB" % (bytes / 1024)
+ if bytes < 1024 * 1024 * 1024:
+ return "%.1f MB" % (bytes / (1024 * 1024))
+ return "%.1f GB" % (bytes / (1024 * 1024 * 1024))
+
+def pluralize(value, _):
+ "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'"
+ try:
+ if int(value) != 1:
+ return 's'
+ except ValueError: # invalid string that's not a number
+ pass
+ except TypeError: # value isn't a string or a number; maybe it's a list?
+ try:
+ if len(value) != 1:
+ return 's'
+ except TypeError: # len() of unsized object
+ pass
+ return ''
+
+def phone2numeric(value, _):
+ "Takes a phone number and converts it in to its numerical equivalent"
+ from django.utils.text import phone2numeric
+ return phone2numeric(value)
+
+def pprint(value, _):
+ "A wrapper around pprint.pprint -- for debugging, really"
+ from pprint import pformat
+ return pformat(value)
+
+# Syntax: template.register_filter(name of filter, callback, has_argument)
+template.register_filter('add', add, True)
+template.register_filter('addslashes', addslashes, False)
+template.register_filter('capfirst', capfirst, False)
+template.register_filter('center', center, True)
+template.register_filter('cut', cut, True)
+template.register_filter('date', date, True)
+template.register_filter('default', default, True)
+template.register_filter('dictsort', dictsort, True)
+template.register_filter('dictsortreversed', dictsortreversed, True)
+template.register_filter('divisibleby', divisibleby, True)
+template.register_filter('escape', escape, False)
+template.register_filter('filesizeformat', filesizeformat, False)
+template.register_filter('first', first, False)
+template.register_filter('fix_ampersands', fix_ampersands, False)
+template.register_filter('floatformat', floatformat, False)
+template.register_filter('get_digit', get_digit, True)
+template.register_filter('join', join, True)
+template.register_filter('length', length, False)
+template.register_filter('length_is', length_is, True)
+template.register_filter('linebreaks', linebreaks, False)
+template.register_filter('linebreaksbr', linebreaksbr, False)
+template.register_filter('linenumbers', linenumbers, False)
+template.register_filter('ljust', ljust, True)
+template.register_filter('lower', lower, False)
+template.register_filter('make_list', make_list, False)
+template.register_filter('phone2numeric', phone2numeric, False)
+template.register_filter('pluralize', pluralize, False)
+template.register_filter('pprint', pprint, False)
+template.register_filter('removetags', removetags, True)
+template.register_filter('random', random, False)
+template.register_filter('rjust', rjust, True)
+template.register_filter('slice', slice_, True)
+template.register_filter('slugify', slugify, False)
+template.register_filter('stringformat', stringformat, True)
+template.register_filter('striptags', striptags, False)
+template.register_filter('time', time, True)
+template.register_filter('timesince', timesince, False)
+template.register_filter('title', title, False)
+template.register_filter('truncatewords', truncatewords, True)
+template.register_filter('unordered_list', unordered_list, False)
+template.register_filter('upper', upper, False)
+template.register_filter('urlencode', urlencode, False)
+template.register_filter('urlize', urlize, False)
+template.register_filter('urlizetrunc', urlizetrunc, True)
+template.register_filter('wordcount', wordcount, False)
+template.register_filter('wordwrap', wordwrap, True)
+template.register_filter('yesno', yesno, True)
diff --git a/django/core/defaulttags.py b/django/core/defaulttags.py
new file mode 100644
index 0000000000..738a7d34b5
--- /dev/null
+++ b/django/core/defaulttags.py
@@ -0,0 +1,743 @@
+"Default tags used by the template system, available to all templates."
+
+import sys
+import template
+
+class CommentNode(template.Node):
+ def render(self, context):
+ return ''
+
+class CycleNode(template.Node):
+ def __init__(self, cyclevars):
+ self.cyclevars = cyclevars
+ self.cyclevars_len = len(cyclevars)
+ self.counter = -1
+
+ def render(self, context):
+ self.counter += 1
+ return self.cyclevars[self.counter % self.cyclevars_len]
+
+class DebugNode(template.Node):
+ def render(self, context):
+ from pprint import pformat
+ output = [pformat(val) for val in context]
+ output.append('\n\n')
+ output.append(pformat(sys.modules))
+ return ''.join(output)
+
+class FilterNode(template.Node):
+ def __init__(self, filters, nodelist):
+ self.filters, self.nodelist = filters, nodelist
+
+ def render(self, context):
+ output = self.nodelist.render(context)
+ # apply filters
+ for f in self.filters:
+ output = template.registered_filters[f[0]][0](output, f[1])
+ return output
+
+class FirstOfNode(template.Node):
+ def __init__(self, vars):
+ self.vars = vars
+
+ def render(self, context):
+ for var in self.vars:
+ value = template.resolve_variable(var, context)
+ if value:
+ return str(value)
+ return ''
+
+class ForNode(template.Node):
+ def __init__(self, loopvar, sequence, reversed, nodelist_loop):
+ self.loopvar, self.sequence = loopvar, sequence
+ self.reversed = reversed
+ self.nodelist_loop = nodelist_loop
+
+ def __repr__(self):
+ if self.reversed:
+ reversed = ' reversed'
+ else:
+ reversed = ''
+ return "<For Node: for %s in %s, tail_len: %d%s>" % \
+ (self.loopvar, self.sequence, len(self.nodelist_loop), reversed)
+
+ def __iter__(self):
+ for node in self.nodelist_loop:
+ yield node
+
+ def get_nodes_by_type(self, nodetype):
+ nodes = []
+ if isinstance(self, nodetype):
+ nodes.append(self)
+ nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype))
+ return nodes
+
+ def render(self, context):
+ nodelist = template.NodeList()
+ if context.has_key('forloop'):
+ parentloop = context['forloop']
+ else:
+ parentloop = {}
+ context.push()
+ try:
+ values = template.resolve_variable_with_filters(self.sequence, context)
+ except template.VariableDoesNotExist:
+ values = []
+ if values is None:
+ values = []
+ len_values = len(values)
+ if self.reversed:
+ # From http://www.python.org/doc/current/tut/node11.html
+ def reverse(data):
+ for index in range(len(data)-1, -1, -1):
+ yield data[index]
+ values = reverse(values)
+ for i, item in enumerate(values):
+ context['forloop'] = {
+ # shortcuts for current loop iteration number
+ 'counter0': i,
+ 'counter': i+1,
+ # boolean values designating first and last times through loop
+ 'first': (i == 0),
+ 'last': (i == len_values - 1),
+ 'parentloop': parentloop,
+ }
+ context[self.loopvar] = item
+ for node in self.nodelist_loop:
+ nodelist.append(node.render(context))
+ context.pop()
+ return nodelist.render(context)
+
+class IfChangedNode(template.Node):
+ def __init__(self, nodelist):
+ self.nodelist = nodelist
+ self._last_seen = None
+
+ 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 ''
+
+class IfNotEqualNode(template.Node):
+ def __init__(self, var1, var2, nodelist):
+ self.var1, self.var2, self.nodelist = var1, var2, nodelist
+
+ def __repr__(self):
+ return "<IfNotEqualNode>"
+
+ def render(self, context):
+ if template.resolve_variable(self.var1, context) != template.resolve_variable(self.var2, context):
+ return self.nodelist.render(context)
+ else:
+ return ''
+
+class IfNode(template.Node):
+ def __init__(self, boolvars, nodelist_true, nodelist_false):
+ self.boolvars = boolvars
+ self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
+
+ def __repr__(self):
+ return "<If node>"
+
+ def __iter__(self):
+ for node in self.nodelist_true:
+ yield node
+ for node in self.nodelist_false:
+ yield node
+
+ def get_nodes_by_type(self, nodetype):
+ nodes = []
+ if isinstance(self, nodetype):
+ nodes.append(self)
+ nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
+ nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
+ return nodes
+
+ def render(self, context):
+ for ifnot, boolvar in self.boolvars:
+ try:
+ value = template.resolve_variable_with_filters(boolvar, context)
+ except template.VariableDoesNotExist:
+ value = None
+ if (value and not ifnot) or (ifnot and not value):
+ return self.nodelist_true.render(context)
+ return self.nodelist_false.render(context)
+
+class RegroupNode(template.Node):
+ def __init__(self, target_var, expression, var_name):
+ self.target_var, self.expression = target_var, expression
+ self.var_name = var_name
+
+ def render(self, context):
+ obj_list = template.resolve_variable_with_filters(self.target_var, context)
+ if obj_list == '': # target_var wasn't found in context; fail silently
+ context[self.var_name] = []
+ return ''
+ output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
+ for obj in obj_list:
+ grouper = template.resolve_variable_with_filters('var.%s' % self.expression, \
+ template.Context({'var': obj}))
+ if output and repr(output[-1]['grouper']) == repr(grouper):
+ output[-1]['list'].append(obj)
+ else:
+ output.append({'grouper': grouper, 'list': [obj]})
+ context[self.var_name] = output
+ return ''
+
+def include_is_allowed(filepath):
+ from django.conf.settings import ALLOWED_INCLUDE_ROOTS
+ for root in ALLOWED_INCLUDE_ROOTS:
+ if filepath.startswith(root):
+ return True
+ return False
+
+class SsiNode(template.Node):
+ def __init__(self, filepath, parsed):
+ self.filepath, self.parsed = filepath, parsed
+
+ def render(self, context):
+ if not include_is_allowed(self.filepath):
+ return '' # Fail silently for invalid includes.
+ try:
+ fp = open(self.filepath, 'r')
+ output = fp.read()
+ fp.close()
+ except IOError:
+ output = ''
+ if self.parsed:
+ try:
+ t = template.Template(output)
+ return t.render(context)
+ except template.TemplateSyntaxError:
+ return '' # Fail silently for invalid included templates.
+ return output
+
+class LoadNode(template.Node):
+ def __init__(self, taglib):
+ self.taglib = taglib
+
+ def load_taglib(taglib):
+ return __import__("django.templatetags.%s" % taglib.split('.')[-1], '', '', [''])
+ load_taglib = staticmethod(load_taglib)
+
+ def render(self, context):
+ "Import the relevant module"
+ try:
+ self.__class__.load_taglib(self.taglib)
+ except ImportError:
+ pass # Fail silently for invalid loads.
+ return ''
+
+class NowNode(template.Node):
+ def __init__(self, format_string):
+ self.format_string = format_string
+
+ def render(self, context):
+ from datetime import datetime
+ from django.utils.dateformat import DateFormat
+ df = DateFormat(datetime.now())
+ return df.format(self.format_string)
+
+class TemplateTagNode(template.Node):
+ mapping = {'openblock': template.BLOCK_TAG_START,
+ 'closeblock': template.BLOCK_TAG_END,
+ 'openvariable': template.VARIABLE_TAG_START,
+ 'closevariable': template.VARIABLE_TAG_END}
+
+ def __init__(self, tagtype):
+ self.tagtype = tagtype
+
+ def render(self, context):
+ return self.mapping.get(self.tagtype, '')
+
+class WidthRatioNode(template.Node):
+ def __init__(self, val_var, max_var, max_width):
+ self.val_var = val_var
+ self.max_var = max_var
+ self.max_width = max_width
+
+ def render(self, context):
+ try:
+ value = template.resolve_variable_with_filters(self.val_var, context)
+ maxvalue = template.resolve_variable_with_filters(self.max_var, context)
+ except template.VariableDoesNotExist:
+ return ''
+ try:
+ value = float(value)
+ maxvalue = float(maxvalue)
+ ratio = (value / maxvalue) * int(self.max_width)
+ except (ValueError, ZeroDivisionError):
+ return ''
+ return str(int(round(ratio)))
+
+def do_comment(parser, token):
+ """
+ Ignore everything between ``{% comment %}`` and ``{% endcomment %}``
+ """
+ nodelist = parser.parse(('endcomment',))
+ parser.delete_first_token()
+ return CommentNode()
+
+def do_cycle(parser, token):
+ """
+ Cycle among the given strings each time this tag is encountered
+
+ Within a loop, cycles among the given strings each time through
+ the loop::
+
+ {% for o in some_list %}
+ <tr class="{% cycle row1,row2 %}">
+ ...
+ </tr>
+ {% endfor %}
+
+ Outside of a loop, give the values a unique name the first time you call
+ it, then use that name each sucessive time through::
+
+ <tr class="{% cycle row1,row2,row3 as rowcolors %}">...</tr>
+ <tr class="{% cycle rowcolors %}">...</tr>
+ <tr class="{% cycle rowcolors %}">...</tr>
+
+ You can use any number of values, seperated by commas. Make sure not to
+ put spaces between the values -- only commas.
+ """
+
+ # Note: This returns the exact same node on each {% cycle name %} call; that
+ # is, the node object returned from {% cycle a,b,c as name %} and the one
+ # returned from {% cycle name %} are the exact same object. This shouldn't
+ # cause problems (heh), but if it does, now you know.
+ #
+ # Ugly hack warning: this stuffs the named template dict into parser so
+ # that names are only unique within each template (as opposed to using
+ # a global variable, which would make cycle names have to be unique across
+ # *all* templates.
+
+ args = token.contents.split()
+ if len(args) < 2:
+ raise template.TemplateSyntaxError("'Cycle' statement requires at least two arguments")
+
+ elif len(args) == 2 and "," in args[1]:
+ # {% cycle a,b,c %}
+ cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks
+ return CycleNode(cyclevars)
+ # {% cycle name %}
+
+ elif len(args) == 2:
+ name = args[1]
+ if not parser._namedCycleNodes.has_key(name):
+ raise template.TemplateSyntaxError("Named cycle '%s' does not exist" % name)
+ return parser._namedCycleNodes[name]
+
+ elif len(args) == 4:
+ # {% cycle a,b,c as name %}
+ if args[2] != 'as':
+ raise template.TemplateSyntaxError("Second 'cycle' argument must be 'as'")
+ cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks
+ name = args[3]
+ node = CycleNode(cyclevars)
+
+ if not hasattr(parser, '_namedCycleNodes'):
+ parser._namedCycleNodes = {}
+
+ parser._namedCycleNodes[name] = node
+ return node
+
+ else:
+ raise template.TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args)
+
+def do_debug(parser, token):
+ "Print a whole load of debugging information, including the context and imported modules"
+ return DebugNode()
+
+def do_filter(parser, token):
+ """
+ Filter the contents of the blog through variable filters.
+
+ Filters can also be piped through each other, and they can have
+ arguments -- just like in variable syntax.
+
+ Sample usage::
+
+ {% filter escape|lower %}
+ This text will be HTML-escaped, and will appear in lowercase.
+ {% endfilter %}
+ """
+ _, rest = token.contents.split(None, 1)
+ _, filters = template.get_filters_from_token('var|%s' % rest)
+ nodelist = parser.parse(('endfilter',))
+ parser.delete_first_token()
+ return FilterNode(filters, nodelist)
+
+def do_firstof(parser, token):
+ """
+ Outputs the first variable passed that is not False.
+
+ Outputs nothing if all the passed variables are False.
+
+ Sample usage::
+
+ {% firstof var1 var2 var3 %}
+
+ This is equivalent to::
+
+ {% if var1 %}
+ {{ var1 }}
+ {% else %}{% if var2 %}
+ {{ var2 }}
+ {% else %}{% if var3 %}
+ {{ var3 }}
+ {% endif %}{% endif %}{% endif %}
+
+ but obviously much cleaner!
+ """
+ bits = token.contents.split()[1:]
+ if len(bits) < 1:
+ raise template.TemplateSyntaxError, "'firstof' statement requires at least one argument"
+ return FirstOfNode(bits)
+
+
+def do_for(parser, token):
+ """
+ Loop over each item in an array.
+
+ For example, to display a list of athletes given ``athlete_list``::
+
+ <ul>
+ {% for athlete in athlete_list %}
+ <li>{{ athlete.name }}</li>
+ {% endfor %}
+ </ul>
+
+ You can also loop over a list in reverse by using
+ ``{% for obj in list reversed %}``.
+
+ The for loop sets a number of variables available within the loop:
+
+ ========================== ================================================
+ Variable Description
+ ========================== ================================================
+ ``forloop.counter`` The current iteration of the loop (1-indexed)
+ ``forloop.counter0`` The current iteration of the loop (0-indexed)
+ ``forloop.first`` True if this is the first time through the loop
+ ``forloop.last`` True if this is the last time through the loop
+ ``forloop.parentloop`` For nested loops, this is the loop "above" the
+ current one
+ ========================== ================================================
+
+ """
+ bits = token.contents.split()
+ if len(bits) == 5 and bits[4] != 'reversed':
+ raise template.TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents
+ if len(bits) not in (4, 5):
+ raise template.TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents
+ if bits[2] != 'in':
+ raise template.TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents
+ loopvar = bits[1]
+ sequence = bits[3]
+ reversed = (len(bits) == 5)
+ nodelist_loop = parser.parse(('endfor',))
+ parser.delete_first_token()
+ return ForNode(loopvar, sequence, reversed, nodelist_loop)
+
+def do_ifnotequal(parser, token):
+ """
+ Output the contents of the block if the two arguments do not equal each other.
+
+ Example::
+
+ {% ifnotequal user.id comment.user_id %}
+ ...
+ {% endifnotequal %}
+ """
+ bits = token.contents.split()
+ if len(bits) != 3:
+ raise template.TemplateSyntaxError, "'ifnotequal' takes two arguments"
+ nodelist = parser.parse(('endifnotequal',))
+ parser.delete_first_token()
+ return IfNotEqualNode(bits[1], bits[2], nodelist)
+
+def do_if(parser, token):
+ """
+ The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
+ (i.e. exists, is not empty, and is not a false boolean value) the contents
+ of the block are output::
+
+ {% if althlete_list %}
+ Number of athletes: {{ althete_list|count }}
+ {% else %}
+ No athletes.
+ {% endif %}
+
+ In the above, if ``athlete_list`` is not empty, the number of athletes will
+ be displayed by the ``{{ athlete_list|count }}`` variable.
+
+ As you can see, the ``if`` tag can take an option ``{% else %} clause that
+ will be displayed if the test fails.
+
+ ``if`` tags may use ``or`` or ``not`` to test a number of variables or to
+ negate a given variable::
+
+ {% if not athlete_list %}
+ There are no athletes.
+ {% endif %}
+
+ {% if athlete_list or coach_list %}
+ There are some athletes or some coaches.
+ {% endif %}
+
+ {% if not athlete_list or coach_list %}
+ There are no athletes or there are some coaches (OK, so
+ writing English translations of boolean logic sounds
+ stupid; it's not my fault).
+ {% endif %}
+
+ For simplicity, ``if`` tags do not allow ``and`` clauses; use nested ``if``s
+ instead::
+
+ {% if athlete_list %}
+ {% if coach_list %}
+ Number of athletes: {{ athlete_list|count }}.
+ Number of coaches: {{ coach_list|count }}.
+ {% endif %}
+ {% endif %}
+ """
+ bits = token.contents.split()
+ del bits[0]
+ if not bits:
+ raise template.TemplateSyntaxError, "'if' statement requires at least one argument"
+ # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d']
+ boolpairs = ' '.join(bits).split(' or ')
+ boolvars = []
+ for boolpair in boolpairs:
+ if ' ' in boolpair:
+ not_, boolvar = boolpair.split()
+ if not_ != 'not':
+ raise template.TemplateSyntaxError, "Expected 'not' in if statement"
+ boolvars.append((True, boolvar))
+ else:
+ boolvars.append((False, boolpair))
+ nodelist_true = parser.parse(('else', 'endif'))
+ token = parser.next_token()
+ if token.contents == 'else':
+ nodelist_false = parser.parse(('endif',))
+ parser.delete_first_token()
+ else:
+ nodelist_false = template.NodeList()
+ return IfNode(boolvars, nodelist_true, nodelist_false)
+
+def do_ifchanged(parser, token):
+ """
+ Check if a value has changed from the last iteration of a loop.
+
+ The 'ifchanged' block tag is used within a loop. It checks its own rendered
+ contents against its previous state and only displays its content if the
+ value has changed::
+
+ <h1>Archive for {{ year }}</h1>
+
+ {% for date in days %}
+ {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
+ <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
+ {% endfor %}
+ """
+ bits = token.contents.split()
+ if len(bits) != 1:
+ raise template.TemplateSyntaxError, "'ifchanged' tag takes no arguments"
+ nodelist = parser.parse(('endifchanged',))
+ parser.delete_first_token()
+ return IfChangedNode(nodelist)
+
+def do_ssi(parser, token):
+ """
+ Output the contents of a given file into the page.
+
+ Like a simple "include" tag, the ``ssi`` tag includes the contents
+ of another file -- which must be specified using an absolute page --
+ in the current page::
+
+ {% ssi /home/html/ljworld.com/includes/right_generic.html %}
+
+ If the optional "parsed" parameter is given, the contents of the included
+ file are evaluated as template code, with the current context::
+
+ {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
+ """
+ bits = token.contents.split()
+ parsed = False
+ if len(bits) not in (2, 3):
+ raise template.TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included"
+ if len(bits) == 3:
+ if bits[2] == 'parsed':
+ parsed = True
+ else:
+ raise template.TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0]
+ return SsiNode(bits[1], parsed)
+
+def do_load(parser, token):
+ """
+ Load a custom template tag set.
+
+ For example, to load the template tags in ``django/templatetags/news/photos.py``::
+
+ {% load news.photos %}
+ """
+ bits = token.contents.split()
+ if len(bits) != 2:
+ raise template.TemplateSyntaxError, "'load' statement takes one argument"
+ taglib = bits[1]
+ # check at compile time that the module can be imported
+ try:
+ LoadNode.load_taglib(taglib)
+ except ImportError:
+ raise template.TemplateSyntaxError, "'%s' is not a valid tag library" % taglib
+ return LoadNode(taglib)
+
+def do_now(parser, token):
+ """
+ Display the date, formatted according to the given string.
+
+ Uses the same format as PHP's ``date()`` function; see http://php.net/date
+ for all the possible values.
+
+ Sample usage::
+
+ It is {% now "jS F Y H:i" %}
+ """
+ bits = token.contents.split('"')
+ if len(bits) != 3:
+ raise template.TemplateSyntaxError, "'now' statement takes one argument"
+ format_string = bits[1]
+ return NowNode(format_string)
+
+def do_regroup(parser, token):
+ """
+ Regroup a list of alike objects by a common attribute.
+
+ This complex tag is best illustrated by use of an example: say that
+ ``people`` is a list of ``Person`` objects that have ``first_name``,
+ ``last_name``, and ``gender`` attributes, and you'd like to display a list
+ that looks like:
+
+ * Male:
+ * George Bush
+ * Bill Clinton
+ * Female:
+ * Margaret Thatcher
+ * Colendeeza Rice
+ * Unknown:
+ * Janet Reno
+
+ The following snippet of template code would accomplish this dubious task::
+
+ {% regroup people by gender as grouped %}
+ <ul>
+ {% for group in grouped %}
+ <li>{{ group.grouper }}
+ <ul>
+ {% for item in group.list %}
+ <li>{{ item }}</li>
+ {% endfor %}
+ </ul>
+ {% endfor %}
+ </ul>
+
+ As you can see, ``{% regroup %}`` populates a variable with a list of
+ objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the
+ item that was grouped by; ``list`` contains the list of objects that share
+ that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female``
+ and ``Unknown``, and ``list`` is the list of people with those genders.
+
+ Note that `{% regroup %}`` does not work when the list to be grouped is not
+ sorted by the key you are grouping by! This means that if your list of
+ people was not sorted by gender, you'd need to make sure it is sorted before
+ using it, i.e.::
+
+ {% regroup people|dictsort:"gender" by gender as grouped %}
+
+ """
+ firstbits = token.contents.split(None, 3)
+ if len(firstbits) != 4:
+ raise template.TemplateSyntaxError, "'regroup' tag takes five arguments"
+ target_var = firstbits[1]
+ if firstbits[2] != 'by':
+ raise template.TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'"
+ lastbits_reversed = firstbits[3][::-1].split(None, 2)
+ if lastbits_reversed[1][::-1] != 'as':
+ raise template.TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'"
+ expression = lastbits_reversed[2][::-1]
+ var_name = lastbits_reversed[0][::-1]
+ return RegroupNode(target_var, expression, var_name)
+
+def do_templatetag(parser, token):
+ """
+ Output one of the bits used to compose template tags.
+
+ Since the template system has no concept of "escaping", to display one of
+ the bits used in template tags, you must use the ``{% templatetag %}`` tag.
+
+ The argument tells which template bit to output:
+
+ ================== =======
+ Argument Outputs
+ ================== =======
+ ``openblock`` ``{%``
+ ``closeblock`` ``%}``
+ ``openvariable`` ``{{``
+ ``closevariable`` ``}}``
+ ================== =======
+ """
+ bits = token.contents.split()
+ if len(bits) != 2:
+ raise template.TemplateSyntaxError, "'templatetag' statement takes one argument"
+ tag = bits[1]
+ if not TemplateTagNode.mapping.has_key(tag):
+ raise template.TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \
+ (tag, TemplateTagNode.mapping.keys())
+ return TemplateTagNode(tag)
+
+def do_widthratio(parser, token):
+ """
+ For creating bar charts and such, this tag calculates the ratio of a given
+ value to a maximum value, and then applies that ratio to a constant.
+
+ For example::
+
+ <img src='bar.gif' height='10' width='{% widthratio this_value max_value 100 %}' />
+
+ Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in
+ the above example will be 88 pixels wide (because 175/200 = .875; .875 *
+ 100 = 87.5 which is rounded up to 88).
+ """
+ bits = token.contents.split()
+ if len(bits) != 4:
+ raise template.TemplateSyntaxError("widthratio takes three arguments")
+ tag, this_value_var, max_value_var, max_width = bits
+ try:
+ max_width = int(max_width)
+ except ValueError:
+ raise template.TemplateSyntaxError("widthratio final argument must be an integer")
+ return WidthRatioNode(this_value_var, max_value_var, max_width)
+
+template.register_tag('comment', do_comment)
+template.register_tag('cycle', do_cycle)
+template.register_tag('debug', do_debug)
+template.register_tag('filter', do_filter)
+template.register_tag('firstof', do_firstof)
+template.register_tag('for', do_for)
+template.register_tag('ifnotequal', do_ifnotequal)
+template.register_tag('if', do_if)
+template.register_tag('ifchanged', do_ifchanged)
+template.register_tag('regroup', do_regroup)
+template.register_tag('ssi', do_ssi)
+template.register_tag('load', do_load)
+template.register_tag('now', do_now)
+template.register_tag('templatetag', do_templatetag)
+template.register_tag('widthratio', do_widthratio)
diff --git a/django/core/exceptions.py b/django/core/exceptions.py
new file mode 100644
index 0000000000..416fdf219d
--- /dev/null
+++ b/django/core/exceptions.py
@@ -0,0 +1,26 @@
+"Global CMS exceptions"
+
+from django.core.template import SilentVariableFailure
+
+class Http404(Exception):
+ pass
+
+class ObjectDoesNotExist(SilentVariableFailure):
+ "The requested object does not exist"
+ pass
+
+class SuspiciousOperation(Exception):
+ "The user did something suspicious"
+ pass
+
+class PermissionDenied(Exception):
+ "The user did not have permission to do that"
+ pass
+
+class ViewDoesNotExist(Exception):
+ "The requested view does not exist"
+ pass
+
+class MiddlewareNotUsed(Exception):
+ "This middleware is not used in this server configuration"
+ pass \ No newline at end of file
diff --git a/django/core/extensions.py b/django/core/extensions.py
new file mode 100644
index 0000000000..c7a7c609af
--- /dev/null
+++ b/django/core/extensions.py
@@ -0,0 +1,79 @@
+"Specialized Context and ModPythonRequest classes for our CMS. Use these!"
+
+from django.core.template import Context
+from django.utils.httpwrappers import ModPythonRequest
+from django.conf.settings import DEBUG, INTERNAL_IPS
+from pprint import pformat
+
+class CMSContext(Context):
+ """This subclass of template.Context automatically populates 'user' and
+ 'messages' in the context. Use this."""
+ def __init__(self, request, dict={}):
+ Context.__init__(self, dict)
+ self['user'] = request.user
+ self['messages'] = request.user.get_and_delete_messages()
+ self['perms'] = PermWrapper(request.user)
+ if DEBUG and request.META['REMOTE_ADDR'] in INTERNAL_IPS:
+ self['debug'] = True
+ from django.core import db
+ self['sql_queries'] = db.db.queries
+
+# 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_permissions())
+ 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)
+
+class CMSRequest(ModPythonRequest):
+ "A special version of ModPythonRequest with support for CMS sessions"
+ def __init__(self, req):
+ ModPythonRequest.__init__(self, req)
+
+ def __repr__(self):
+ return '<CMSRequest\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s,\nuser:%s>' % \
+ (self.path, pformat(self.GET), pformat(self.POST), pformat(self.COOKIES),
+ pformat(self.META), pformat(self.user))
+
+ def _load_session_and_user(self):
+ from django.models.auth import sessions
+ from django.conf.settings import AUTH_SESSION_COOKIE
+ session_cookie = self.COOKIES.get(AUTH_SESSION_COOKIE, '')
+ try:
+ self._session = sessions.get_session_from_cookie(session_cookie)
+ self._user = self._session.get_user()
+ except sessions.SessionDoesNotExist:
+ from django.parts.auth import anonymoususers
+ self._session = None
+ self._user = anonymoususers.AnonymousUser()
+
+ def _get_session(self):
+ if not hasattr(self, '_session'):
+ self._load_session_and_user()
+ return self._session
+
+ def _set_session(self, session):
+ self._session = session
+
+ def _get_user(self):
+ if not hasattr(self, '_user'):
+ self._load_session_and_user()
+ return self._user
+
+ def _set_user(self, user):
+ self._user = user
+
+ session = property(_get_session, _set_session)
+ user = property(_get_user, _set_user)
diff --git a/django/core/formfields.py b/django/core/formfields.py
new file mode 100644
index 0000000000..745f52036f
--- /dev/null
+++ b/django/core/formfields.py
@@ -0,0 +1,759 @@
+from django.core import validators
+from django.core.exceptions import PermissionDenied
+from django.utils.html import escape
+from django.utils.text import fix_microsoft_characters
+
+FORM_FIELD_ID_PREFIX = 'id_'
+
+class EmptyValue(Exception):
+ "This is raised when empty data is provided"
+ pass
+
+class Manipulator:
+ # List of permission strings. User must have at least one to manipulate.
+ # None means everybody has permission.
+ required_permission = ''
+
+ def __init__(self):
+ # List of FormField objects
+ self.fields = []
+
+ def __getitem__(self, field_name):
+ "Looks up field by field name; raises KeyError on failure"
+ for field in self.fields:
+ if field.field_name == field_name:
+ return field
+ raise KeyError, "Field %s not found" % field_name
+
+ def __delitem__(self, field_name):
+ "Deletes the field with the given field name; raises KeyError on failure"
+ for i, field in enumerate(self.fields):
+ if field.field_name == field_name:
+ del self.fields[i]
+ return
+ raise KeyError, "Field %s not found" % field_name
+
+ def check_permissions(self, user):
+ """Confirms user has required permissions to use this manipulator; raises
+ PermissionDenied on failure."""
+ if self.required_permission is None:
+ return
+ if user.has_perm(self.required_permission):
+ return
+ raise PermissionDenied
+
+ def prepare(self, new_data):
+ """
+ Makes any necessary preparations to new_data, in place, before data has
+ been validated.
+ """
+ for field in self.fields:
+ field.prepare(new_data)
+
+ def get_validation_errors(self, new_data):
+ "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('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):
+ "Saves the changes and returns the new object"
+ # changes is a dictionary-like object keyed by field_name
+ raise NotImplementedError
+
+ def do_html2python(self, new_data):
+ """
+ Convert the data from HTML data types to Python datatypes, changing the
+ object in place. This happens after validation but before storage. This
+ must happen after validation because html2python functions aren't
+ expected to deal with invalid input.
+ """
+ for field in self.fields:
+ if new_data.has_key(field.field_name):
+ new_data.setlist(field.field_name,
+ [field.__class__.html2python(data) for data in new_data.getlist(field.field_name)])
+ else:
+ try:
+ # individual fields deal with None values themselves
+ new_data.setlist(field.field_name, [field.__class__.html2python(None)])
+ except EmptyValue:
+ new_data.setlist(field.field_name, [])
+
+class FormWrapper:
+ """
+ A wrapper linking a Manipulator to the template system.
+ This allows dictionary-style lookups of formfields. It also handles feeding
+ prepopulated data and validation error messages to the formfield objects.
+ """
+ def __init__(self, manipulator, data, error_dict):
+ self.manipulator, self.data = manipulator, data
+ self.error_dict = error_dict
+
+ def __repr__(self):
+ return repr(self.data)
+
+ def __getitem__(self, key):
+ for field in self.manipulator.fields:
+ if field.field_name == key:
+ if hasattr(field, 'requires_data_list') and hasattr(self.data, 'getlist'):
+ data = self.data.getlist(field.field_name)
+ else:
+ data = self.data.get(field.field_name, None)
+ if data is None:
+ data = ''
+ return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, []))
+ raise KeyError
+
+ def has_errors(self):
+ return self.error_dict != {}
+
+class FormFieldWrapper:
+ "A bridge between the template system and an individual form field. Used by FormWrapper."
+ def __init__(self, formfield, data, error_list):
+ self.formfield, self.data, self.error_list = formfield, data, error_list
+ self.field_name = self.formfield.field_name # for convenience in templates
+
+ def __str__(self):
+ "Renders the field"
+ return str(self.formfield.render(self.data))
+
+ def __repr__(self):
+ return '<FormFieldWrapper for "%s">' % self.formfield.field_name
+
+ def field_list(self):
+ """
+ Like __str__(), but returns a list. Use this when the field's render()
+ method returns a list.
+ """
+ return self.formfield.render(self.data)
+
+ def errors(self):
+ return self.error_list
+
+ def html_error_list(self):
+ if self.errors():
+ return '<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()])
+ else:
+ return ''
+
+class FormFieldCollection(FormFieldWrapper):
+ "A utility class that gives the template access to a dict of FormFieldWrappers"
+ def __init__(self, formfield_dict):
+ self.formfield_dict = formfield_dict
+
+ def __str__(self):
+ return str(self.formfield_dict)
+
+ def __getitem__(self, template_key):
+ "Look up field by template key; raise KeyError on failure"
+ return self.formfield_dict[template_key]
+
+ def __repr__(self):
+ return "<FormFieldCollection: %s>" % self.formfield_dict
+
+ def errors(self):
+ "Returns list of all errors in this collection's formfields"
+ errors = []
+ for field in self.formfield_dict.values():
+ errors.extend(field.errors())
+ return errors
+
+class FormField:
+ """Abstract class representing a form field.
+
+ Classes that extend FormField should define the following attributes:
+ field_name
+ The field's name for use by programs.
+ validator_list
+ A list of validation tests (callback functions) that the data for
+ this field must pass in order to be added or changed.
+ is_required
+ A Boolean. Is it a required field?
+ Subclasses should also implement a render(data) method, which is responsible
+ for rending the form field in XHTML.
+ """
+ def __str__(self):
+ return self.render('')
+
+ def __repr__(self):
+ return 'FormField "%s"' % self.field_name
+
+ def prepare(self, new_data):
+ "Hook for doing something to new_data (in place) before validation."
+ pass
+
+ def html2python(data):
+ "Hook for converting an HTML datatype (e.g. 'on' for checkboxes) to a Python type"
+ return data
+ html2python = staticmethod(html2python)
+
+ def render(self, data):
+ raise NotImplementedError
+
+####################
+# GENERIC WIDGETS #
+####################
+
+class TextField(FormField):
+ def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[]):
+ self.field_name = field_name
+ self.length, self.maxlength = length, maxlength
+ self.is_required = is_required
+ self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list
+
+ def isValidLength(self, data, form):
+ if data and self.maxlength and len(data) > self.maxlength:
+ raise validators.ValidationError, "Ensure your text is less than %s characters." % self.maxlength
+
+ def hasNoNewlines(self, data, form):
+ if data and '\n' in data:
+ raise validators.ValidationError, "Line breaks are not allowed here."
+
+ def render(self, data):
+ if data is None:
+ data = ''
+ maxlength = ''
+ if self.maxlength:
+ maxlength = 'maxlength="%s" ' % self.maxlength
+ if isinstance(data, unicode):
+ data = data.encode('utf-8')
+ return '<input type="text" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
+ self.field_name, self.length, escape(data), maxlength)
+
+ def html2python(data):
+ if data:
+ return fix_microsoft_characters(data)
+ return data
+ html2python = staticmethod(html2python)
+
+class PasswordField(TextField):
+ def render(self, data):
+ # value is always blank because we never want to redisplay it
+ return '<input type="password" id="%s" class="v%s%s" name="%s" value="" />' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
+ self.field_name)
+
+class LargeTextField(TextField):
+ def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=[], maxlength=None):
+ self.field_name = field_name
+ self.rows, self.cols, self.is_required = rows, cols, is_required
+ self.validator_list = validator_list[:]
+ if maxlength:
+ self.validator_list.append(self.isValidLength)
+ self.maxlength = maxlength
+
+ def render(self, data):
+ if data is None:
+ data = ''
+ if isinstance(data, unicode):
+ data = data.encode('utf-8')
+ return '<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
+ self.field_name, self.rows, self.cols, escape(data))
+
+class HiddenField(FormField):
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ self.field_name, self.is_required = field_name, is_required
+ self.validator_list = validator_list[:]
+
+ def render(self, data):
+ return '<input type="hidden" id="%s" name="%s" value="%s" />' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.field_name, escape(data))
+
+class CheckboxField(FormField):
+ def __init__(self, field_name, checked_by_default=False):
+ self.field_name = field_name
+ self.checked_by_default = checked_by_default
+ self.is_required, self.validator_list = False, [] # because the validator looks for these
+
+ def render(self, data):
+ checked_html = ''
+ if data or (data is '' and self.checked_by_default):
+ checked_html = ' checked="checked"'
+ return '<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__,
+ self.field_name, checked_html)
+
+ def html2python(data):
+ "Convert value from browser ('on' or '') to a Python boolean"
+ if data == 'on':
+ return True
+ return False
+ html2python = staticmethod(html2python)
+
+class SelectField(FormField):
+ def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=[]):
+ self.field_name = field_name
+ # choices is a list of (value, human-readable key) tuples because order matters
+ self.choices, self.size, self.is_required = choices, size, is_required
+ self.validator_list = [self.isValidChoice] + validator_list
+
+ def render(self, data):
+ output = ['<select id="%s" class="v%s%s" name="%s" size="%s">' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
+ self.field_name, self.size)]
+ str_data = str(data) # normalize to string
+ for value, display_name in self.choices:
+ selected_html = ''
+ if str(value) == str_data:
+ selected_html = ' selected="selected"'
+ output.append(' <option value="%s"%s>%s</option>' % (escape(value), selected_html, display_name))
+ output.append(' </select>')
+ return '\n'.join(output)
+
+ def isValidChoice(self, data, form):
+ 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; '%s' is not in %s." % (str_data, str_choices)
+
+class NullSelectField(SelectField):
+ "This SelectField converts blank fields to None"
+ def html2python(data):
+ if not data:
+ return None
+ return data
+ html2python = staticmethod(html2python)
+
+class RadioSelectField(FormField):
+ def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=[]):
+ self.field_name = field_name
+ # choices is a list of (value, human-readable key) tuples because order matters
+ self.choices, self.is_required = choices, is_required
+ self.validator_list = [self.isValidChoice] + validator_list
+ self.ul_class = ul_class
+
+ def render(self, data):
+ """
+ Returns a special object, RadioFieldRenderer, that is iterable *and*
+ has a default str() rendered output.
+
+ This allows for flexible use in templates. You can just use the default
+ rendering:
+
+ {{ field_name }}
+
+ ...which will output the radio buttons in an unordered list.
+ Or, you can manually traverse each radio option for special layout:
+
+ {% for option in field_name.field_list %}
+ {{ option.field }} {{ option.label }}<br />
+ {% endfor %}
+ """
+ class RadioFieldRenderer:
+ def __init__(self, datalist, ul_class):
+ self.datalist, self.ul_class = datalist, ul_class
+ def __str__(self):
+ "Default str() output for this radio field -- a <ul>"
+ output = ['<ul%s>' % (self.ul_class and ' class="%s"' % self.ul_class or '')]
+ output.extend(['<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist])
+ output.append('</ul>')
+ return ''.join(output)
+ def __iter__(self):
+ for d in self.datalist:
+ yield d
+ def __len__(self):
+ return len(self.datalist)
+ datalist = []
+ str_data = str(data) # normalize to string
+ for i, (value, display_name) in enumerate(self.choices):
+ selected_html = ''
+ if str(value) == str_data:
+ selected_html = ' checked="checked"'
+ datalist.append({
+ 'value': value,
+ 'name': display_name,
+ 'field': '<input type="radio" id="%s" name="%s" value="%s"%s/>' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name + '_' + str(i), self.field_name, value, selected_html),
+ 'label': '<label for="%s">%s</label>' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name + '_' + str(i), display_name),
+ })
+ return RadioFieldRenderer(datalist, self.ul_class)
+
+ def isValidChoice(self, data, form):
+ 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; '%s' is not in %s." % (str_data, str_choices)
+
+class NullBooleanField(SelectField):
+ "This SelectField provides 'Yes', 'No' and 'Unknown', mapping results to True, False or None"
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ SelectField.__init__(self, field_name, choices=[('1', 'Unknown'), ('2', 'Yes'), ('3', 'No')],
+ is_required=is_required, validator_list=validator_list)
+
+ def render(self, data):
+ if data is None: data = '1'
+ elif data == True: data = '2'
+ elif data == False: data = '3'
+ return SelectField.render(self, data)
+
+ def html2python(data):
+ return {'1': None, '2': True, '3': False}[data]
+ html2python = staticmethod(html2python)
+
+class SelectMultipleField(SelectField):
+ requires_data_list = True
+ def render(self, data):
+ output = ['<select id="%s" class="v%s%s" name="%s" size="%s" multiple="multiple">' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
+ self.field_name, self.size)]
+ str_data_list = map(str, data) # normalize to strings
+ for value, choice in self.choices:
+ selected_html = ''
+ if str(value) in str_data_list:
+ selected_html = ' selected="selected"'
+ output.append(' <option value="%s"%s>%s</option>' % (escape(value), selected_html, choice))
+ output.append(' </select>')
+ return '\n'.join(output)
+
+ def isValidChoice(self, field_data, all_data):
+ # data is something like ['1', '2', '3']
+ 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; '%s' is not in %s." % (val, str_choices)
+
+ def html2python(data):
+ if data is None:
+ raise EmptyValue
+ return data
+ html2python = staticmethod(html2python)
+
+class CheckboxSelectMultipleField(SelectMultipleField):
+ """
+ This has an identical interface to SelectMultipleField, except the rendered
+ widget is different. Instead of a <select multiple>, this widget outputs a
+ <ul> of <input type="checkbox">es.
+
+ Of course, that results in multiple form elements for the same "single"
+ field, so this class's prepare() method flattens the split data elements
+ back into the single list that validators, renderers and save() expect.
+ """
+ requires_data_list = True
+ def __init__(self, field_name, choices=[], validator_list=[]):
+ SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list)
+
+ def prepare(self, new_data):
+ # new_data has "split" this field into several fields, so flatten it
+ # back into a single list.
+ data_list = []
+ for value, _ in self.choices:
+ if new_data.get('%s%s' % (self.field_name, value), '') == 'on':
+ data_list.append(value)
+ new_data.setlist(self.field_name, data_list)
+
+ def render(self, data):
+ output = ['<ul>']
+ str_data_list = map(str, data) # normalize to strings
+ for value, choice in self.choices:
+ checked_html = ''
+ if str(value) in str_data_list:
+ checked_html = ' checked="checked"'
+ field_name = '%s%s' % (self.field_name, value)
+ output.append('<li><input type="checkbox" id="%s%s" class="v%s" name="%s"%s /> <label for="%s%s">%s</label></li>' % \
+ (FORM_FIELD_ID_PREFIX, field_name, self.__class__.__name__, field_name, checked_html,
+ FORM_FIELD_ID_PREFIX, field_name, choice))
+ output.append('</ul>')
+ return '\n'.join(output)
+
+####################
+# FILE UPLOADS #
+####################
+
+class FileUploadField(FormField):
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ self.field_name, self.is_required = field_name, is_required
+ self.validator_list = [self.isNonEmptyFile] + validator_list
+
+ def isNonEmptyFile(self, field_data, all_data):
+ if not field_data['content']:
+ raise validators.CriticalValidationError, "The submitted file is empty."
+
+ def render(self, data):
+ return '<input type="file" id="%s" class="v%s" name="%s" />' % \
+ (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__,
+ self.field_name)
+
+ def html2python(data):
+ if data is None:
+ raise EmptyValue
+ return data
+ html2python = staticmethod(html2python)
+
+class ImageUploadField(FileUploadField):
+ "A FileUploadField that raises CriticalValidationError if the uploaded file isn't an image."
+ def __init__(self, *args, **kwargs):
+ FileUploadField.__init__(self, *args, **kwargs)
+ self.validator_list.insert(0, self.isValidImage)
+
+ def isValidImage(self, field_data, all_data):
+ try:
+ validators.isValidImage(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+####################
+# INTEGERS/FLOATS #
+####################
+
+class IntegerField(TextField):
+ def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]):
+ validator_list = [self.isInteger] + validator_list
+ TextField.__init__(self, field_name, length, maxlength, is_required, validator_list)
+
+ def isInteger(self, field_data, all_data):
+ try:
+ validators.isInteger(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+ def html2python(data):
+ if data == '' or data is None:
+ return None
+ return int(data)
+ html2python = staticmethod(html2python)
+
+class SmallIntegerField(IntegerField):
+ def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=[]):
+ validator_list = [self.isSmallInteger] + validator_list
+ IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)
+
+ 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."
+
+class PositiveIntegerField(IntegerField):
+ def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]):
+ validator_list = [self.isPositive] + validator_list
+ IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)
+
+ def isPositive(self, field_data, all_data):
+ if int(field_data) < 0:
+ raise validators.CriticalValidationError, "Enter a positive number."
+
+class PositiveSmallIntegerField(IntegerField):
+ def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=[]):
+ validator_list = [self.isPositiveSmall] + validator_list
+ IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)
+
+ 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."
+
+class FloatField(TextField):
+ def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=[]):
+ self.max_digits, self.decimal_places = max_digits, decimal_places
+ validator_list = [self.isValidFloat] + validator_list
+ TextField.__init__(self, field_name, max_digits+1, max_digits+1, is_required, validator_list)
+
+ def isValidFloat(self, field_data, all_data):
+ v = validators.IsValidFloat(self.max_digits, self.decimal_places)
+ try:
+ v(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+ def html2python(data):
+ if data == '' or data is None:
+ return None
+ return float(data)
+ html2python = staticmethod(html2python)
+
+####################
+# DATES AND TIMES #
+####################
+
+class DatetimeField(TextField):
+ """A FormField that automatically converts its data to a datetime.datetime object.
+ The data should be in the format YYYY-MM-DD HH:MM:SS."""
+ def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[]):
+ self.field_name = field_name
+ self.length, self.maxlength = length, maxlength
+ self.is_required = is_required
+ self.validator_list = [validators.isValidANSIDatetime] + validator_list
+
+ def html2python(data):
+ "Converts the field into a datetime.datetime object"
+ import datetime
+ date, time = data.split()
+ y, m, d = date.split('-')
+ timebits = time.split(':')
+ h, mn = timebits[:2]
+ if len(timebits) > 2:
+ s = int(timebits[2])
+ else:
+ s = 0
+ return datetime.datetime(int(y), int(m), int(d), int(h), int(mn), s)
+ html2python = staticmethod(html2python)
+
+class DateField(TextField):
+ """A FormField that automatically converts its data to a datetime.date object.
+ The data should be in the format YYYY-MM-DD."""
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ validator_list = [self.isValidDate] + validator_list
+ TextField.__init__(self, field_name, length=10, maxlength=10,
+ is_required=is_required, validator_list=validator_list)
+
+ def isValidDate(self, field_data, all_data):
+ try:
+ validators.isValidANSIDate(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+ def html2python(data):
+ "Converts the field into a datetime.date object"
+ import time, datetime
+ try:
+ time_tuple = time.strptime(data, '%Y-%m-%d')
+ return datetime.date(*time_tuple[0:3])
+ except (ValueError, TypeError):
+ return None
+ html2python = staticmethod(html2python)
+
+class TimeField(TextField):
+ """A FormField that automatically converts its data to a datetime.time object.
+ The data should be in the format HH:MM:SS."""
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ validator_list = [self.isValidTime] + validator_list
+ TextField.__init__(self, field_name, length=8, maxlength=8,
+ is_required=is_required, validator_list=validator_list)
+
+ def isValidTime(self, field_data, all_data):
+ try:
+ validators.isValidANSITime(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+ def html2python(data):
+ "Converts the field into a datetime.time object"
+ import time, datetime
+ try:
+ try:
+ time_tuple = time.strptime(data, '%H:%M:%S')
+ except ValueError: # seconds weren't provided
+ time_tuple = time.strptime(data, '%H:%M')
+ return datetime.time(*time_tuple[3:6])
+ except ValueError:
+ return None
+ html2python = staticmethod(html2python)
+
+####################
+# INTERNET-RELATED #
+####################
+
+class EmailField(TextField):
+ "A convenience FormField for validating e-mail addresses"
+ def __init__(self, field_name, length=50, is_required=False, validator_list=[]):
+ validator_list = [self.isValidEmail] + validator_list
+ TextField.__init__(self, field_name, length, maxlength=75,
+ is_required=is_required, validator_list=validator_list)
+
+ def isValidEmail(self, field_data, all_data):
+ try:
+ validators.isValidEmail(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+class URLField(TextField):
+ "A convenience FormField for validating URLs"
+ def __init__(self, field_name, length=50, is_required=False, validator_list=[]):
+ validator_list = [self.isValidURL] + validator_list
+ TextField.__init__(self, field_name, length=length, maxlength=200,
+ is_required=is_required, validator_list=validator_list)
+
+ def isValidURL(self, field_data, all_data):
+ try:
+ validators.isValidURL(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+class IPAddressField(TextField):
+ def html2python(data):
+ return data or None
+ html2python = staticmethod(html2python)
+
+####################
+# MISCELLANEOUS #
+####################
+
+class PhoneNumberField(TextField):
+ "A convenience FormField for validating phone numbers (e.g. '630-555-1234')"
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ validator_list = [self.isValidPhone] + validator_list
+ TextField.__init__(self, field_name, length=12, maxlength=12,
+ is_required=is_required, validator_list=validator_list)
+
+ def isValidPhone(self, field_data, all_data):
+ try:
+ validators.isValidPhone(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+class USStateField(TextField):
+ "A convenience FormField for validating U.S. states (e.g. 'IL')"
+ def __init__(self, field_name, is_required=False, validator_list=[]):
+ validator_list = [self.isValidUSState] + validator_list
+ TextField.__init__(self, field_name, length=2, maxlength=2,
+ is_required=is_required, validator_list=validator_list)
+
+ def isValidUSState(self, field_data, all_data):
+ try:
+ validators.isValidUSState(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+ def html2python(data):
+ return data.upper() # Should always be stored in upper case
+ html2python = staticmethod(html2python)
+
+class CommaSeparatedIntegerField(TextField):
+ "A convenience FormField for validating comma-separated integer fields"
+ def __init__(self, field_name, maxlength=None, is_required=False, validator_list=[]):
+ validator_list = [self.isCommaSeparatedIntegerList] + validator_list
+ TextField.__init__(self, field_name, length=20, maxlength=maxlength,
+ is_required=is_required, validator_list=validator_list)
+
+ def isCommaSeparatedIntegerList(self, field_data, all_data):
+ try:
+ validators.isCommaSeparatedIntegerList(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
+
+class XMLLargeTextField(LargeTextField):
+ """
+ A LargeTextField with an XML validator. The schema_path argument is the
+ full path to a Relax NG compact schema to validate against.
+ """
+ def __init__(self, field_name, schema_path, **kwargs):
+ self.schema_path = schema_path
+ kwargs.setdefault('validator_list', []).insert(0, self.isValidXML)
+ LargeTextField.__init__(self, field_name, **kwargs)
+
+ def isValidXML(self, field_data, all_data):
+ v = validators.RelaxNGCompact(self.schema_path)
+ try:
+ v(field_data, all_data)
+ except validators.ValidationError, e:
+ raise validators.CriticalValidationError, e.messages
diff --git a/django/core/handler.py b/django/core/handler.py
new file mode 100644
index 0000000000..1412ee6838
--- /dev/null
+++ b/django/core/handler.py
@@ -0,0 +1,157 @@
+import os
+from django.utils import httpwrappers
+
+# NOTE: do *not* import settings (or any module which eventually imports
+# settings) until after CoreHandler has been called; otherwise os.environ
+# won't be set up correctly (with respect to settings).
+
+class ImproperlyConfigured(Exception):
+ pass
+
+class CoreHandler:
+
+ def __init__(self):
+ self._request_middleware = self._view_middleware = self._response_middleware = None
+
+ def __call__(self, req):
+ # mod_python fakes the environ, and thus doesn't process SetEnv. This fixes that
+ os.environ.update(req.subprocess_env)
+
+ # 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 we need to set up middleware, now that settings works we can do it now.
+ if self._request_middleware is None:
+ self.load_middleware()
+
+ try:
+ request = self.get_request(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)
+
+ # Convert our custom HttpResponse object back into the mod_python req.
+ httpwrappers.populate_apache_request(response, req)
+ return 0 # mod_python.apache.OK
+
+ def load_middleware(self):
+ """
+ Populate middleware lists from settings.MIDDLEWARE_CLASSES.
+
+ Must be called after the environment is fixed (see __call__).
+ """
+ from django.conf import settings
+ from django.core import exceptions
+ self._request_middleware = []
+ self._view_middleware = []
+ self._response_middleware = []
+ for middleware_path in settings.MIDDLEWARE_CLASSES:
+ dot = middleware_path.rindex('.')
+ mw_module, mw_classname = middleware_path[:dot], middleware_path[dot+1:]
+ try:
+ mod = __import__(mw_module, '', '', [''])
+ except ImportError, e:
+ raise ImproperlyConfigured, 'Error importing middleware %s: "%s"' % (mw_module, e)
+ try:
+ mw_class = getattr(mod, mw_classname)
+ except AttributeError:
+ raise ImproperlyConfigured, 'Middleware module "%s" does not define a "%s" class' % (mw_module, mw_classname)
+
+ try:
+ mw_instance = mw_class()
+ except exceptions.MiddlewareNotUsed:
+ continue
+
+ if hasattr(mw_instance, 'process_request'):
+ self._request_middleware.append(mw_instance.process_request)
+ if hasattr(mw_instance, 'process_view'):
+ self._view_middleware.append(mw_instance.process_view)
+ if hasattr(mw_instance, 'process_response'):
+ self._response_middleware.insert(0, mw_instance.process_response)
+
+ def get_request(self, req):
+ "Returns an HttpRequest object for the given mod_python req object"
+ from django.core.extensions import CMSRequest
+ return CMSRequest(req)
+
+ def get_response(self, path, request):
+ "Returns an HttpResponse object for the given HttpRequest"
+ from django.core import db, exceptions, urlresolvers
+ from django.core.mail import mail_admins
+ from django.conf.settings import DEBUG, INTERNAL_IPS, ROOT_URLCONF
+
+ # Apply request middleware
+ for middleware_method in self._request_middleware:
+ response = middleware_method(request)
+ if response:
+ return response
+
+ conf_module = __import__(ROOT_URLCONF, '', '', [''])
+ resolver = urlresolvers.RegexURLResolver(conf_module.urlpatterns)
+ try:
+ callback, param_dict = resolver.resolve(path)
+ # Apply view middleware
+ for middleware_method in self._view_middleware:
+ response = middleware_method(request, callback, param_dict)
+ if response:
+ return response
+ return callback(request, **param_dict)
+ except exceptions.Http404:
+ if DEBUG:
+ return self.get_technical_error_response(is404=True)
+ else:
+ resolver = urlresolvers.Error404Resolver(conf_module.handler404)
+ callback, param_dict = resolver.resolve()
+ return callback(request, **param_dict)
+ except db.DatabaseError:
+ db.db.rollback()
+ if DEBUG:
+ return self.get_technical_error_response()
+ else:
+ subject = 'Database error (%s IP)' % (request.META['REMOTE_ADDR'] in INTERNAL_IPS and 'internal' or 'EXTERNAL')
+ message = "%s\n\n%s" % (self._get_traceback(), request)
+ mail_admins(subject, message, fail_silently=True)
+ return self.get_friendly_error_response(request, conf_module)
+ except exceptions.PermissionDenied:
+ return httpwrappers.HttpResponseForbidden('<h1>Permission denied</h1>')
+ except: # Handle everything else, including SuspiciousOperation, etc.
+ if DEBUG:
+ return self.get_technical_error_response()
+ else:
+ subject = 'Coding error (%s IP)' % (request.META['REMOTE_ADDR'] in INTERNAL_IPS and 'internal' or 'EXTERNAL')
+ message = "%s\n\n%s" % (self._get_traceback(), request)
+ mail_admins(subject, message, fail_silently=True)
+ return self.get_friendly_error_response(request, conf_module)
+
+ def get_friendly_error_response(self, request, conf_module):
+ """
+ Returns an HttpResponse that displays a PUBLIC error message for a
+ fundamental database or coding error.
+ """
+ from django.core import urlresolvers
+ resolver = urlresolvers.Error404Resolver(conf_module.handler500)
+ callback, param_dict = resolver.resolve()
+ return callback(request, **param_dict)
+
+ def get_technical_error_response(self, is404=False):
+ """
+ Returns an HttpResponse that displays a TECHNICAL error message for a
+ fundamental database or coding error.
+ """
+ error_string = "<pre>There's been an error:\n\n%s</pre>" % self._get_traceback()
+ responseClass = is404 and httpwrappers.HttpResponseNotFound or httpwrappers.HttpResponseServerError
+ return responseClass(error_string)
+
+ def _get_traceback(self):
+ "Helper function to return the traceback as a string"
+ import sys, traceback
+ return '\n'.join(traceback.format_exception(*sys.exc_info()))
+
+def handler(req):
+ return CoreHandler()(req)
diff --git a/django/core/mail.py b/django/core/mail.py
new file mode 100644
index 0000000000..c6969c16ca
--- /dev/null
+++ b/django/core/mail.py
@@ -0,0 +1,51 @@
+"""
+Use this for e-mailing
+"""
+
+from django.conf.settings import DEFAULT_FROM_EMAIL, EMAIL_HOST
+from email.MIMEText import MIMEText
+import smtplib
+
+def send_mail(subject, message, from_email, recipient_list, fail_silently=False):
+ """
+ Easy wrapper for sending a single message to a recipient list. All members
+ of the recipient list will see the other recipients in the 'To' field.
+ """
+ return send_mass_mail([[subject, message, from_email, recipient_list]], fail_silently)
+
+def send_mass_mail(datatuple, fail_silently=False):
+ """
+ Given a datatuple of (subject, message, from_email, recipient_list), sends
+ each message to each recipient list. Returns the number of e-mails sent.
+
+ If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
+ """
+ try:
+ server = smtplib.SMTP(EMAIL_HOST)
+ except:
+ if fail_silently:
+ return
+ raise
+ num_sent = 0
+ for subject, message, from_email, recipient_list in datatuple:
+ if not recipient_list:
+ continue
+ from_email = from_email or DEFAULT_FROM_EMAIL
+ msg = MIMEText(message)
+ 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()
+ return num_sent
+
+def mail_admins(subject, message, fail_silently=False):
+ "Sends a message to the admins, as defined by the ADMINS constant in settings.py."
+ from django.conf.settings import ADMINS, SERVER_EMAIL
+ send_mail('[CMS] ' + subject, message, SERVER_EMAIL, [a[1] for a in ADMINS], fail_silently)
+
+def mail_managers(subject, message, fail_silently=False):
+ "Sends a message to the managers, as defined by the MANAGERS constant in settings.py"
+ from django.conf.settings import MANAGERS, SERVER_EMAIL
+ send_mail('[CMS] ' + subject, message, SERVER_EMAIL, [a[1] for a in MANAGERS], fail_silently)
diff --git a/django/core/meta.py b/django/core/meta.py
new file mode 100644
index 0000000000..be203efa37
--- /dev/null
+++ b/django/core/meta.py
@@ -0,0 +1,2142 @@
+from django.core import formfields, validators
+from django.core import db
+from django.core.exceptions import ObjectDoesNotExist
+from django.conf import settings
+import copy, datetime, os, re, sys, types
+
+# The values to use for "blank" in SelectFields. Will be appended to the start of most "choices" lists.
+BLANK_CHOICE_DASH = [("", "---------")]
+BLANK_CHOICE_NONE = [("", "None")]
+
+# Admin stages.
+ADD, CHANGE, BOTH = 1, 2, 3
+
+# Values for Relation.edit_inline_type.
+TABULAR, STACKED = 1, 2
+
+# Values for filter_interface.
+HORIZONTAL, VERTICAL = 1, 2
+
+# Random entropy string used by "default" param.
+NOT_PROVIDED = 'oijpwojefiojpanv'
+
+# 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 = '__'
+
+RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
+
+####################
+# HELPER FUNCTIONS #
+####################
+
+# capitalizes first letter of string
+capfirst = lambda x: x and x[0].upper() + x[1:]
+
+# 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 curry(*args, **kwargs):
+ def _curried(*moreargs, **morekwargs):
+ return args[0](*(args[1:]+moreargs), **dict(kwargs.items() + morekwargs.items()))
+ return _curried
+
+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__', []):
+ _installed_modules_cache.append(__import__('django.models.%s' % submodule, '', '', ['']))
+ 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 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, ManyToMany):
+ 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', 'ASC'),)
+ else:
+ self.order_with_respect_to = None
+ self.module_constants = module_constants or {}
+ # Alter the admin attribute so that the 'fields' members are lists of
+ # field objects -- not lists of field names.
+ if admin:
+ # Be sure to use admin.copy(), because otherwise we'll be editing a
+ # reference of admin, which will in turn affect the copy in
+ # self._orig_init_args.
+ self.admin = admin.copy()
+ for fieldset in self.admin.fields:
+ admin_fields = []
+ for field_name_or_list in fieldset[1]['fields']:
+ if isinstance(field_name_or_list, basestring):
+ admin_fields.append([self.get_field(field_name_or_list)])
+ else:
+ admin_fields.append([self.get_field(field_name) for field_name in field_name_or_list])
+ fieldset[1]['fields'] = admin_fields
+ else:
+ self.admin = None
+
+ # Calculate one_to_one_field.
+ self.one_to_one_field = None
+ for f in self.fields:
+ if isinstance(f.rel, OneToOne):
+ 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('id', 'ID', primary_key=True))
+ self.pk = self.fields[0]
+
+ 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 ' + ','.join(['%s%s %s' % (pre, f, order) for f, order in self.ordering])
+
+ 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_rel_object_method_name(self, rel_opts, rel_field):
+ # This method encapsulates the logic that decides what name to give a
+ # method that retrieves related many-to-one objects. Usually it just
+ # uses the lower-cased object_name, but if the related object is in
+ # another app, its 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 = rel_field.rel.related_name or rel_opts.object_name.lower()
+ if self.app_label != rel_opts.app_label:
+ rel_obj_name = '%s_%s' % (rel_opts.app_label, rel_obj_name)
+ return rel_obj_name
+
+ 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((klass._meta, f))
+ if self.has_related_links:
+ # Manually add RelatedLink objects, which are a special case.
+ core = 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(core.RelatedLink._meta.get_field('object_id'))
+ link_field.rel = ManyToOne(self.get_model_module().Klass, 'related_links', 'id',
+ num_in_admin=3, min_num_in_admin=3, edit_inline=True, edit_inline_type=TABULAR,
+ lookup_overrides={
+ 'content_type__package__label__exact': self.app_label,
+ 'content_type__python_module_name__exact': self.module_name
+ })
+ rel_objs.append((core.RelatedLink._meta, link_field))
+ self._all_related_objects = rel_objs
+ return rel_objs
+
+ def get_inline_related_objects(self):
+ return [(a, b) for a, b in self.get_all_related_objects() if b.rel.edit_inline]
+
+ 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:
+ try:
+ for f in klass._meta.many_to_many:
+ if f.rel and self == f.rel.to:
+ rel_objs.append((klass._meta, f))
+ raise StopIteration
+ except StopIteration:
+ continue
+ 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):
+ """
+ Returns True if this object's admin form has at least one of the given
+ field_type (e.g. FileField).
+ """
+ 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 rel_obj, rel_field in self.get_inline_related_objects():
+ for f in rel_obj.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
+
+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)
+
+ # 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:
+ if not attrs.has_key('fields'):
+ attrs['fields'] = list(bases[0]._meta._orig_init_args['fields'][:])
+ if attrs.has_key('ignore_fields'):
+ ignore_fields = attrs.pop('ignore_fields')
+ new_fields = []
+ for i, f in enumerate(attrs['fields']):
+ if f.name not in ignore_fields:
+ new_fields.append(f)
+ attrs['fields'] = new_fields
+ if attrs.has_key('add_fields'):
+ attrs['fields'].extend(attrs.pop('add_fields'))
+ if 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 = 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 = {}
+ for k, v in attrs.items():
+ if not callable(v) and k != '__module__':
+ meta_overrides[k] = attrs.pop(k)
+ opts = bases[0]._meta.copy(**meta_overrides)
+ opts.object_name = name
+ del meta_overrides
+ else:
+ opts = Options(
+ # If the module_name wasn't given, use the class name
+ # in lowercase, plus a trailing "s" -- a poor-man's
+ # pluralization.
+ module_name = attrs.pop('module_name', name.lower() + 's'),
+ # If the verbose_name wasn't given, use the class name,
+ # converted from InitialCaps to "lowercase with spaces".
+ verbose_name = attrs.pop('verbose_name',
+ re.sub('([A-Z])', ' \\1', name).lower().strip()),
+ verbose_name_plural = attrs.pop('verbose_name_plural', ''),
+ db_table = attrs.pop('db_table', ''),
+ fields = attrs.pop('fields'),
+ ordering = attrs.pop('ordering', None),
+ unique_together = attrs.pop('unique_together', None),
+ admin = attrs.pop('admin', None),
+ has_related_links = attrs.pop('has_related_links', False),
+ where_constraints = attrs.pop('where_constraints', None),
+ object_name = name,
+ app_label = attrs.pop('app_label', None),
+ exceptions = attrs.pop('exceptions', None),
+ permissions = attrs.pop('permissions', None),
+ get_latest_by = attrs.pop('get_latest_by', None),
+ order_with_respect_to = attrs.pop('order_with_respect_to', None),
+ module_constants = attrs.pop('module_constants', None),
+ )
+
+ # 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.
+ # 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['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
+ # Add "get_thingie" methods for many-to-one related objects.
+ # EXAMPLES: Choice.get_poll(), Story.get_dateline()
+ if isinstance(f.rel, ManyToOne):
+ 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.rel.name] = func
+
+ for f in opts.many_to_many:
+ # Add "get_thingie" methods for many-to-many related objects.
+ # EXAMPLES: Poll.get_sites(), Story.get_bylines()
+ 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' % f.name] = 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.
+ 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_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:
+ 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, f, True))
+ setattr(new_class, 'get_previous_by_%s' % f.name, curry(method_get_next_or_previous, new_mod.get_object, 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 rel_opts, rel_field in model._meta.get_all_related_objects():
+ rel_field.rel.to = opts
+ for rel_opts, rel_field in model._meta.get_all_related_many_to_many_objects():
+ rel_field.rel.to = opts
+ break
+
+ return new_class
+
+class Model:
+ __metaclass__ = ModelBase
+
+############################################
+# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
+############################################
+
+# CORE METHODS #############################
+
+def method_init(opts, self, *args, **kwargs):
+ for i, arg in enumerate(args):
+ setattr(self, opts.fields[i].name, arg)
+ for k, v in kwargs.items():
+ try:
+ opts.get_field(k, many_to_many=False)
+ except FieldDoesNotExist:
+ raise TypeError, "'%s' is an invalid keyword argument for this function" % k
+ setattr(self, k, v)
+
+def method_eq(opts, self, other):
+ return isinstance(other, self.__class__) and getattr(self, opts.pk.name) == getattr(other, opts.pk.name)
+
+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()
+ add = not bool(getattr(self, opts.pk.name))
+ for f in non_pks:
+ f.pre_save(self, getattr(self, f.name), add)
+ db_values = [f.get_db_prep_save(getattr(self, f.name), add) for f in non_pks]
+ # OneToOne objects are a special case because there's no AutoField, and the
+ # primary key field is set manually.
+ if isinstance(opts.pk.rel, OneToOne):
+ cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
+ (opts.db_table, ','.join(['%s=%%s' % f.name for f in non_pks]),
+ opts.pk.name), db_values + [getattr(self, opts.pk.name)])
+ if cursor.rowcount == 0: # If nothing was updated, add the record.
+ field_names = [f.name for f in opts.fields]
+ placeholders = ['%s'] * len(field_names)
+ cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \
+ (opts.db_table, ','.join(field_names), ','.join(placeholders)),
+ [f.get_db_prep_save(getattr(self, f.name), add=True) for f in opts.fields])
+ else:
+ if not add:
+ cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
+ (opts.db_table, ','.join(['%s=%%s' % f.name for f in non_pks]),
+ opts.pk.name), db_values + [getattr(self, opts.pk.name)])
+ else:
+ field_names = [f.name for f in non_pks]
+ placeholders = ['%s'] * len(field_names)
+ if opts.order_with_respect_to:
+ field_names.append('_order')
+ placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \
+ (opts.db_table, opts.order_with_respect_to.name))
+ db_values.append(getattr(self, opts.order_with_respect_to.name))
+ cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \
+ (opts.db_table, ','.join(field_names), ','.join(placeholders)), db_values)
+ setattr(self, opts.pk.name, db.get_last_insert_id(cursor, opts.db_table, opts.pk.name))
+ 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.name) is not None, "%r can't be deleted because it doesn't have an ID."
+ cursor = db.db.cursor()
+ for rel_opts, rel_field in opts.get_all_related_objects():
+ rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field)
+ if isinstance(rel_field.rel, OneToOne):
+ 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 rel_opts, rel_field in opts.get_all_related_many_to_many_objects():
+ cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (rel_field.get_m2m_db_table(rel_opts),
+ self._meta.object_name.lower()), [getattr(self, opts.pk.name)])
+ cursor.execute("DELETE FROM %s WHERE %s=%%s" % (opts.db_table, opts.pk.name), [getattr(self, opts.pk.name)])
+ db.db.commit()
+ setattr(self, opts.pk.name, None)
+ for f in opts.fields:
+ if isinstance(f, FileField) and getattr(self, f.name):
+ 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)
+
+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', 'ASC'),),
+ where=['_order > (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.name),
+ '%s=%%s' % order_field.name], limit=1,
+ params=[getattr(self, opts.pk.name), getattr(self, order_field.name)])
+ 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', 'DESC'),),
+ where=['_order < (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.name),
+ '%s=%%s' % order_field.name], limit=1,
+ params=[getattr(self, opts.pk.name), getattr(self, order_field.name)])
+ 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.rel.get_cache_name()
+ if not hasattr(self, cache_var):
+ val = getattr(self, field_with_rel.name)
+ mod = field_with_rel.rel.to.get_model_module()
+ if val is None:
+ raise getattr(mod, '%sDoesNotExist' % field_with_rel.rel.to.object_name)
+ retrieved_obj = mod.get_object(**{'%s__exact' % field_with_rel.rel.field_name: val})
+ setattr(self, cache_var, retrieved_obj)
+ return getattr(self, cache_var)
+
+# Handles getting many-to-many related objects.
+# Example: Poll.get_sites()
+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_id AND b.%s_id = %%s %s" % \
+ (','.join(['a.%s' % f.name for f in rel.fields]), rel.db_table,
+ field_with_rel.get_m2m_db_table(self._meta), rel.pk.name,
+ rel.object_name.lower(), self._meta.object_name.lower(), rel.get_order_sql('a'))
+ cursor = db.db.cursor()
+ cursor.execute(sql, [getattr(self, self._meta.pk.name)])
+ 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):
+ id_list = map(int, id_list) # normalize to integers
+ current_ids = [obj.id 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.name)
+ if ids_to_delete:
+ sql = "DELETE FROM %s WHERE %s_id = %%s AND %s_id IN (%s)" % (m2m_table, self._meta.object_name.lower(), rel.object_name.lower(), ','.join(map(str, ids_to_delete)))
+ cursor.execute(sql, [this_id])
+ if ids_to_add:
+ sql = "INSERT INTO %s (%s_id, %s_id) VALUES (%%s, %%s)" % (m2m_table, self._meta.object_name.lower(), rel.object_name.lower())
+ 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):
+ kwargs['%s__exact' % rel_field.name] = getattr(self, rel_field.rel.field_name)
+ kwargs.update(rel_field.rel.lookup_overrides)
+ return getattr(rel_mod, method_name)(**kwargs)
+
+# 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.name 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.name] = None
+ init_kwargs[rel_field.name] = getattr(self, rel_field.rel.field_name)
+ 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, rel_mod, rel_field, self, **kwargs):
+ kwargs['%s__id__exact' % rel_field.name] = self.id
+ 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.name)
+ cursor = db.db.cursor()
+ cursor.execute("DELETE FROM %s WHERE %s_id = %%s" % (m2m_table, rel.object_name.lower()), [this_id])
+ if rel_field.rel.orderable:
+ sql = "INSERT INTO %s (%s_id, %s_id, _order) VALUES (%%s, %%s, %%s)" % (m2m_table, rel.object_name.lower(), rel_opts.object_name.lower())
+ cursor.executemany(sql, [(this_id, j, i) for i, j in enumerate(id_list)])
+ else:
+ sql = "INSERT INTO %s (%s_id, %s_id) VALUES (%%s, %%s)" % (m2m_table, rel.object_name.lower(), rel_opts.object_name.lower())
+ 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 _order = %%s WHERE %s = %%s AND %s = %%s" % (ordered_obj.db_table, ordered_obj.order_with_respect_to.name, ordered_obj.pk.name)
+ 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 _order" % (ordered_obj.pk.name, ordered_obj.db_table, ordered_obj.order_with_respect_to.name)
+ 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, field, is_next, self, **kwargs):
+ kwargs.setdefault('where', []).append('%s %s %%s' % (field.name, (is_next and '>' or '<')))
+ kwargs.setdefault('params', []).append(str(getattr(self, field.name)))
+ kwargs['order_by'] = ((field.name, (is_next and 'ASC' or 'DESC')),)
+ kwargs['limit'] = 1
+ return get_object_func(**kwargs)
+
+# FILE-RELATED METHODS #####################
+
+def method_get_file_filename(field, self):
+ return os.path.join(settings.MEDIA_ROOT, getattr(self, field.name))
+
+def method_get_file_url(field, self):
+ if getattr(self, field.name): # value is not blank
+ import urlparse
+ return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.name))
+ 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.name, filename)
+ fp = open(getattr(self, 'get_%s_filename' % field.name)(), 'w')
+ 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):
+ try:
+ return '%s%s %s %%s' % (table_prefix, field_name, db.OPERATOR_MAPPING[lookup_type])
+ except KeyError:
+ pass
+ if lookup_type in ('range', 'year'):
+ return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name)
+ elif lookup_type in ('month', 'day'):
+ return "EXTRACT('%s' FROM %s%s) = %%s" % (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):
+ 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.rel.get_cache_name(), rel_obj)
+ return obj, index_end
+
+def function_get_list(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)
+ obj_list = []
+ fill_cache = kwargs.get('select_related')
+ index_end = len(opts.fields)
+ for row in cursor.fetchall():
+ 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])
+ obj_list.append(obj)
+ return obj_list
+
+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_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 _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_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_table, new_prefix))
+ db_table = new_prefix
+ cache_tables_seen.append(db_table)
+ where.append('%s.%s = %s.%s' % (old_prefix, f.name, db_table, f.rel.field_name))
+ select.extend(['%s.%s' % (db_table, f2.name) 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 == '_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)
+ 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 = 't%s' % table_count
+ table_count += 1
+ tables.append('%s %s' % (f.get_m2m_db_table(current_opts), rel_table_alias))
+ join_where.append('%s.%s = %s.%s_id' % (current_table_alias, current_opts.pk.name,
+ rel_table_alias, current_opts.object_name.lower()))
+ # 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' % (f.rel.to.db_table, new_table_alias))
+ join_where.append('%s.%s_id = %s.%s' % (rel_table_alias, f.rel.to.object_name.lower(),
+ new_table_alias, f.rel.to.pk.name))
+ 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.rel.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.name, 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' % (f.rel.to.db_table, new_table_alias))
+ join_where.append('%s.%s = %s.%s' % (current_table_alias, f.name, new_table_alias, f.rel.to.pk.name))
+ 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+'.', current, 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):
+ select = ["%s.%s" % (opts.db_table, f.name) for f in opts.fields]
+ tables = [opts.db_table] + (kwargs.get('tables') and kwargs['tables'][:] or [])
+ 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', False):
+ select.extend(['(%s) AS %s' % (s[1], s[0]) for s in kwargs['select']])
+
+ # ORDER BY clause
+ order_by = []
+ for i, j in kwargs.get('order_by', opts.ordering):
+ if j == "RANDOM":
+ order_by.append("RANDOM()")
+ else:
+ # Append 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 i and i not in [k[0] for k in kwargs.get('select', [])]:
+ order_by.append("%s.%s %s" % (opts.db_table, i, j))
+ else:
+ order_by.append("%s %s" % (i, j))
+ order_by = ", ".join(order_by)
+
+ # LIMIT and OFFSET clauses
+ if kwargs.get('limit') is not None:
+ limit_sql = " LIMIT %s " % kwargs['limit']
+ if kwargs.get('offset') is not None and kwargs['offset'] != 0:
+ limit_sql += "OFFSET %s " % kwargs['offset']
+ else:
+ 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['id_list']
+ assert id_list != [], "get_in_bulk() cannot be passed an empty list."
+ kwargs['where'] = ["%s.id IN (%s)" % (opts.db_table, ",".join(map(str, id_list)))]
+ obj_list = function_get_list(opts, klass, **kwargs)
+ return dict([(o.id, o) for o in obj_list])
+
+def function_get_latest(opts, klass, does_not_exist_exception, **kwargs):
+ kwargs['order_by'] = ((opts.get_latest_by, "DESC"),)
+ kwargs['limit'] = 1
+ return function_get_object(opts, klass, does_not_exist_exception, **kwargs)
+
+def function_get_date_list(opts, field, *args, **kwargs):
+ kind = args and args[0] or kwargs['kind']
+ assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'."
+ order = 'ASC'
+ if kwargs.has_key('_order'):
+ order = kwargs['_order']
+ del kwargs['_order']
+ 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' % (opts.db_table, field.name))
+ select, sql, params = function_get_sql_clause(opts, **kwargs)
+ sql = "SELECT DATE_TRUNC(%%s, %s.%s) %s GROUP BY 1 ORDER BY 1 %s" % (opts.db_table, field.name, sql, order)
+ cursor = db.db.cursor()
+ cursor.execute(sql, [kind] + params)
+ return [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)
+ 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):
+ 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(**{'%s__exact' % opts.pk.name: 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 propogate 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.name, f.get_default()) for f in opts.fields])
+ params[opts.pk.name] = 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 f.editable and (not f.rel or not f.rel.edit_inline):
+ self.fields.extend(f.get_manipulator_fields(opts, self, change))
+
+ # Add fields for related objects.
+ for rel_opts, rel_field in opts.get_inline_related_objects():
+ if change:
+ count = getattr(self.original_object, 'get_%s_count' % opts.get_rel_object_method_name(rel_opts, rel_field))()
+ count += rel_field.rel.num_extra_on_change
+ if rel_field.rel.min_num_in_admin:
+ count = max(count, rel_field.rel.min_num_in_admin)
+ if rel_field.rel.max_num_in_admin:
+ count = min(count, rel_field.rel.max_num_in_admin)
+ else:
+ count = rel_field.rel.num_in_admin
+ for f in rel_opts.fields + rel_opts.many_to_many:
+ if f.editable and f != rel_field and (not f.primary_key or (f.primary_key and change)):
+ for i in range(count):
+ self.fields.extend(f.get_manipulator_fields(rel_opts, self, change, name_prefix='%s.%d.' % (rel_opts.object_name.lower(), i), rel=True))
+
+ # 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):
+ from django.utils.datastructures import DotExpandedDict
+ params = {}
+ for f in opts.fields:
+ # Fields with auto_now_add are another special case; they should keep
+ # their original value in the change stage.
+ if change and getattr(f, 'auto_now_add', False):
+ params[f.name] = getattr(self.original_object, f.name)
+ else:
+ params[f.name] = f.get_manipulator_new_data(new_data)
+
+ if change:
+ params[opts.pk.name] = 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.name)) != str(getattr(new_object, f.name)):
+ self.fields_changed.append(f.verbose_name)
+
+ # Save many-to-many objects. Example: Poll.set_sites()
+ for f in opts.many_to_many:
+ if not f.rel.edit_inline:
+ was_changed = getattr(new_object, 'set_%s' % f.name)(new_data.getlist(f.name))
+ if change and was_changed:
+ self.fields_changed.append(f.verbose_name)
+
+ # Save many-to-one objects. Example: Add the Choice objects for a Poll.
+ for rel_opts, rel_field in opts.get_inline_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': ['']})]
+ obj_list = DotExpandedDict(new_data.data)[rel_opts.object_name.lower()].items()
+ obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0])))
+ params = {}
+
+ # For each related item...
+ for _, rel_new_data in obj_list:
+
+ # 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.
+ if change:
+ old_rel_obj = None
+ if rel_new_data[rel_opts.pk.name][0]:
+ try:
+ old_rel_obj = getattr(self.original_object, 'get_%s' % opts.get_rel_object_method_name(rel_opts, rel_field))(**{'%s__exact' % rel_opts.pk.name: rel_new_data[rel_opts.pk.name][0]})
+ except ObjectDoesNotExist:
+ pass
+
+ for f in rel_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 change and (isinstance(f, FileField) or not f.editable):
+ if rel_new_data.get(rel_opts.pk.name, False) and rel_new_data[rel_opts.pk.name][0]:
+ params[f.name] = getattr(old_rel_obj, f.name)
+ else:
+ params[f.name] = f.get_default()
+ elif f == rel_field:
+ params[f.name] = getattr(new_object, rel_field.rel.field_name)
+ elif add and isinstance(f, AutoField):
+ params[f.name] = None
+ else:
+ params[f.name] = f.get_manipulator_new_data(rel_new_data, rel=True)
+ # Related links are a special case, because we have to
+ # manually set the "content_type_id" field.
+ if opts.has_related_links and rel_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 = rel_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 rel_opts.fields:
+ 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, change, 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 "%r"' % (rel_opts.verbose_name, new_rel_obj))
+ else:
+ for f in rel_opts.fields:
+ if not f.primary_key and f != rel_field and str(getattr(old_rel_obj, f.name)) != str(getattr(new_rel_obj, f.name)):
+ self.fields_changed.append('%s for %s "%r"' % (f.verbose_name, rel_opts.verbose_name, new_rel_obj))
+
+ # Save many-to-many objects.
+ for f in rel_opts.many_to_many:
+ if not f.rel.edit_inline:
+ was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.name])
+ if change and was_changed:
+ self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, rel_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 rel_new_data.has_key(rel_opts.pk.name) and rel_new_data[rel_opts.pk.name][0]:
+ new_rel_obj.delete()
+ self.fields_deleted.append('%s "%r"' % (rel_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_validator_unique(f, opts, self, field_data, all_data):
+ "Validates that the value is unique for this field."
+ try:
+ old_obj = opts.get_model_module().get_object(**{'%s__exact' % f.name: field_data})
+ except ObjectDoesNotExist:
+ return
+ if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name):
+ return
+ raise validators.ValidationError, "%s with this %s already exists." % (capfirst(opts.verbose_name), f.verbose_name)
+
+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]
+ kwargs = {'%s__iexact' % field_name_list[0]: field_data}
+ for f in field_list[1:]:
+ field_val = all_data.get(f.name, None)
+ if field_val is None:
+ # This will be caught by another validator, assuming the field
+ # doesn't have blank=True.
+ return
+ 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.name) == getattr(old_obj, opts.pk.name):
+ pass
+ else:
+ raise validators.ValidationError, "%s with this %s already exists for the given %s." % \
+ (capfirst(opts.verbose_name), field_list[0].verbose_name, 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__iexact' % from_field.name: field_data, '%s__year' % date_field.name: date_val.year}
+ 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.name) == getattr(old_obj, opts.pk.name):
+ 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))
+
+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(**{'id__iexact': field_data})
+ except ObjectDoesNotExist:
+ raise validators.ValidationError, "Please enter a valid %s." % f.verbose_name
+
+####################
+# FIELDS #
+####################
+
+class Field(object):
+
+ # Designates whether empty strings fundamentally are allowed at the
+ # database level.
+ empty_strings_allowed = True
+
+ def __init__(self, name, verbose_name, primary_key=False,
+ maxlength=None, unique=False, blank=False, null=False, db_index=None,
+ 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=''):
+ self.name, self.verbose_name = name, verbose_name
+ self.primary_key = primary_key
+ self.maxlength, self.unique = maxlength, unique
+ self.blank, self.null = blank, null
+ self.core, self.rel, self.default = core, rel, default
+ self.editable = editable
+ self.validator_list = validator_list or []
+ self.prepopulate_from = prepopulate_from
+ self.unique_for_date, self.unique_for_month = unique_for_date, unique_for_month
+ self.unique_for_year = unique_for_year
+ self.choices = choices or []
+ self.radio_admin = radio_admin
+ self.help_text = help_text
+ if rel and isinstance(rel, ManyToMany):
+ self.help_text += ' 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, OneToOne) or isinstance(rel, ManyToOne):
+ self.db_index = True
+ else:
+ self.db_index = False
+ else:
+ self.db_index = db_index
+
+ def pre_save(self, obj, value, add):
+ """
+ Hook for altering the object obj based on the value of this field and
+ and on the add/change status.
+ """
+ pass
+
+ def get_db_prep_save(self, value, add):
+ "Returns field's value prepared for saving into a database."
+ return value
+
+ def get_db_prep_lookup(self, lookup_type, value):
+ "Returns field's value prepared for database lookup."
+ if lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne', 'month', 'day'):
+ return [value]
+ elif lookup_type == 'range':
+ return value
+ elif lookup_type == 'year':
+ return ['%s-01-01' % value, '%s-12-31' % value]
+ elif lookup_type in ('contains', 'icontains'):
+ return ["%%%s%%" % prep_for_like_query(value)]
+ elif lookup_type == 'iexact':
+ return [prep_for_like_query(value)]
+ elif lookup_type == 'startswith':
+ return ["%s%%" % prep_for_like_query(value)]
+ elif lookup_type == 'endswith':
+ return ["%%%s" % prep_for_like_query(value)]
+ elif lookup_type == 'isnull':
+ return []
+ raise TypeError, "Field has invalid lookup: %s" % lookup_type
+
+ def get_m2m_db_table(self, original_opts):
+ "Returns the name of the DB table for this field's relationship."
+ return '%s_%s' % (original_opts.db_table, self.name)
+
+ def has_default(self):
+ "Returns a boolean of whether this field has a default value."
+ return self.default != NOT_PROVIDED
+
+ def get_default(self):
+ "Returns the default value for this field."
+ if self.default != NOT_PROVIDED:
+ if hasattr(self.default, '__get_value__'):
+ return self.default.__get_value__()
+ return self.default
+ if self.null:
+ return None
+ return ""
+
+ def get_manipulator_field_names(self, name_prefix):
+ """
+ Returns a list of field names that this object adds to the manipulator.
+ """
+ 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.
+ """
+ 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, ManyToOne):
+ 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['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE)
+ 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()
+ elif self.choices:
+ if self.radio_admin:
+ field_objs = [formfields.RadioSelectField]
+ params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE)
+ params['ul_class'] = get_ul_class(self.radio_admin)
+ else:
+ field_objs = [formfields.SelectField]
+ params['choices'] = self.get_choices()
+ else:
+ field_objs = self.get_manipulator_field_objs()
+
+ # Add the "unique" validator(s).
+ for field_name_list in opts.unique_together:
+ if field_name_list[0] == self.name:
+ params['validator_list'].append(getattr(manipulator, 'isUnique%s' % '_'.join(field_name_list)))
+
+ # Add the "unique for..." validator(s).
+ if self.unique_for_date:
+ params['validator_list'].append(getattr(manipulator, 'isUnique%sFor%s' % (self.name, self.unique_for_date)))
+ if self.unique_for_month:
+ params['validator_list'].append(getattr(manipulator, 'isUnique%sFor%s' % (self.name, self.unique_for_month)))
+ if self.unique_for_year:
+ params['validator_list'].append(getattr(manipulator, 'isUnique%sFor%s' % (self.name, self.unique_for_year)))
+ if self.unique:
+ params['validator_list'].append(curry(manipulator_validator_unique, self, opts, manipulator))
+
+ # Only add is_required=True if the field cannot be blank. Primary keys
+ # are a special case, and fields in a related context should set this
+ # as False, because they'll be caught by a separate validator --
+ # RequiredIfOtherFieldGiven.
+ params['is_required'] = not self.blank and not self.primary_key and not rel
+
+ # 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.
+ if rel and not self.blank and not isinstance(self, AutoField) and not isinstance(self, FileField):
+ # First, get the core fields, if any.
+ core_field_names = []
+ for f in opts.fields:
+ if f.core and f != self:
+ core_field_names.extend(f.get_manipulator_field_names(name_prefix))
+ # Now, if there are any, add the validator to this FormField.
+ if core_field_names:
+ params['validator_list'].append(validators.RequiredIfOtherFieldsGiven(core_field_names, "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_manipulator_new_data(self, new_data, rel=False):
+ """
+ Given the full new_data dictionary (from the manipulator), returns this
+ field's data.
+ """
+ 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
+
+ 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.name), repr(x)) for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)]
+
+class AutoField(Field):
+ empty_strings_allowed = False
+ def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False):
+ 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)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.HiddenField]
+
+ def get_manipulator_new_data(self, new_data, rel=False):
+ if not rel:
+ return None
+ return Field.get_manipulator_new_data(self, new_data, rel)
+
+class BooleanField(Field):
+ def __init__(self, name, verbose_name, **kwargs):
+ kwargs['blank'] = True
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.CheckboxField]
+
+class CharField(Field):
+ def get_manipulator_field_objs(self):
+ return [formfields.TextField]
+
+class CommaSeparatedIntegerField(CharField):
+ def get_manipulator_field_objs(self):
+ return [formfields.CommaSeparatedIntegerField]
+
+class DateField(Field):
+ empty_strings_allowed = False
+ def __init__(self, name, verbose_name, auto_now=False, auto_now_add=False, **kwargs):
+ 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, name, verbose_name, **kwargs)
+
+ 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 pre_save(self, obj, value, add):
+ if self.auto_now or (self.auto_now_add and add):
+ setattr(obj, self.name, datetime.datetime.now())
+
+ def get_db_prep_save(self, value, add):
+ # Casts dates into string format for entry into database.
+ if value is not None:
+ value = value.strftime('%Y-%m-%d')
+ return Field.get_db_prep_save(self, value, add)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.DateField]
+
+class DateTimeField(DateField):
+ def get_db_prep_save(self, value, add):
+ # Casts dates into string format for entry into database.
+ if value is not None:
+ value = value.strftime('%Y-%m-%d %H:%M:%S')
+ return Field.get_db_prep_save(self, value, add)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.DateField, formfields.TimeField]
+
+ def get_manipulator_field_names(self, name_prefix):
+ return [name_prefix + self.name + '_date', name_prefix + self.name + '_time']
+
+ def get_manipulator_new_data(self, new_data, rel=False):
+ date_field, time_field = self.get_manipulator_field_names('')
+ if rel:
+ d = new_data.get(date_field, [None])[0]
+ t = new_data.get(time_field, [None])[0]
+ else:
+ d = new_data.get(date_field, None)
+ t = new_data.get(time_field, None)
+ if d is not None and t is not None:
+ return datetime.datetime.combine(d, t)
+ return self.get_default()
+
+class EmailField(Field):
+ def get_manipulator_field_objs(self):
+ return [formfields.EmailField]
+
+class FileField(Field):
+ def __init__(self, name, verbose_name, upload_to='', **kwargs):
+ self.upload_to = upload_to
+ Field.__init__(self, name, verbose_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)
+
+ if not self.blank:
+ if rel:
+ # This validator makes sure FileFields work in a related context.
+ class RequiredFileField:
+ def __init__(self, other_field_names, other_file_field_name):
+ self.other_field_names = other_field_names
+ self.other_file_field_name = other_file_field_name
+ self.always_test = True
+ def __call__(self, field_data, all_data):
+ if not all_data.get(self.other_file_field_name, False):
+ c = validators.RequiredIfOtherFieldsGiven(self.other_field_names, "This field is required.")
+ c(field_data, all_data)
+ # First, get the core fields, if any.
+ core_field_names = []
+ for f in opts.fields:
+ if f.core and f != self:
+ core_field_names.extend(f.get_manipulator_field_names(name_prefix))
+ # Now, if there are any, add the validator to this FormField.
+ if core_field_names:
+ field_list[0].validator_list.append(RequiredFileField(core_field_names, field_list[1].field_name))
+ else:
+ v = validators.RequiredIfOtherFieldNotGiven(field_list[1].field_name, "This field is required.")
+ v.always_test = True
+ field_list[0].validator_list.append(v)
+ field_list[0].is_required = field_list[1].is_required = False
+
+ # If the raw path is passed in, validate it's under the MEDIA_ROOT.
+ def isWithinMediaRoot(field_data, all_data):
+ f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
+ if not f.startswith(os.path.normpath(settings.MEDIA_ROOT)):
+ raise validators.ValidationError, "Enter a valid filename."
+ field_list[1].validator_list.append(isWithinMediaRoot)
+ return field_list
+
+ def get_manipulator_field_objs(self):
+ return [formfields.FileUploadField, formfields.HiddenField]
+
+ def get_manipulator_field_names(self, name_prefix):
+ return [name_prefix + self.name + '_file', name_prefix + self.name]
+
+ def save_file(self, new_data, new_object, original_object, change, rel):
+ upload_field_name = self.get_manipulator_field_names('')[0]
+ if new_data.get(upload_field_name, False):
+ if rel:
+ getattr(new_object, 'save_%s_file' % self.name)(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"])
+ else:
+ getattr(new_object, 'save_%s_file' % self.name)(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"])
+
+ def get_directory_name(self):
+ return os.path.normpath(datetime.datetime.now().strftime(self.upload_to))
+
+ def get_filename(self, filename):
+ from django.utils.text import get_valid_filename
+ f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
+ return os.path.normpath(f)
+
+class FloatField(Field):
+ empty_strings_allowed = False
+ def __init__(self, name, verbose_name, max_digits, decimal_places, **kwargs):
+ self.max_digits, self.decimal_places = max_digits, decimal_places
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [curry(formfields.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)]
+
+class ImageField(FileField):
+ def __init__(self, name, verbose_name, width_field=None, height_field=None, **kwargs):
+ self.width_field, self.height_field = width_field, height_field
+ FileField.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.ImageUploadField, formfields.HiddenField]
+
+ def save_file(self, new_data, new_object, original_object, change, rel):
+ FileField.save_file(self, new_data, new_object, original_object, change, rel)
+ # If the image has height and/or width field(s) and they haven't
+ # changed, set the width and/or height field(s) back to their original
+ # values.
+ if change and (self.width_field or self.height_field):
+ if self.width_field:
+ setattr(new_object, self.width_field, getattr(original_object, self.width_field))
+ if self.height_field:
+ setattr(new_object, self.height_field, getattr(original_object, self.height_field))
+ new_object.save()
+
+class IntegerField(Field):
+ empty_strings_allowed = False
+ def get_manipulator_field_objs(self):
+ return [formfields.IntegerField]
+
+class IPAddressField(Field):
+ def __init__(self, name, verbose_name, **kwargs):
+ kwargs['maxlength'] = 15
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.IPAddressField]
+
+class NullBooleanField(Field):
+ def __init__(self, name, verbose_name, **kwargs):
+ kwargs['null'] = True
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.NullBooleanField]
+
+class PhoneNumberField(IntegerField):
+ def get_manipulator_field_objs(self):
+ return [formfields.PhoneNumberField]
+
+class PositiveIntegerField(IntegerField):
+ def get_manipulator_field_objs(self):
+ return [formfields.PositiveIntegerField]
+
+class PositiveSmallIntegerField(IntegerField):
+ def get_manipulator_field_objs(self):
+ return [formfields.PositiveSmallIntegerField]
+
+class SlugField(Field):
+ def __init__(self, name, verbose_name, **kwargs):
+ kwargs['maxlength'] = 50
+ kwargs.setdefault('validator_list', []).append(validators.isAlphaNumeric)
+ # Set db_index=True unless it's been set manually.
+ if not kwargs.has_key('db_index'):
+ kwargs['db_index'] = True
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.TextField]
+
+class SmallIntegerField(IntegerField):
+ def get_manipulator_field_objs(self):
+ return [formfields.SmallIntegerField]
+
+class TextField(Field):
+ def get_manipulator_field_objs(self):
+ return [formfields.LargeTextField]
+
+class TimeField(Field):
+ empty_strings_allowed = False
+ def __init__(self, name, verbose_name, auto_now=False, auto_now_add=False, **kwargs):
+ 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, name, verbose_name, **kwargs)
+
+ 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 pre_save(self, obj, value, add):
+ if self.auto_now or (self.auto_now_add and add):
+ setattr(obj, self.name, datetime.datetime.now().time())
+
+ def get_db_prep_save(self, value, add):
+ # Casts dates into string format for entry into database.
+ if value is not None:
+ value = value.strftime('%H:%M:%S')
+ return Field.get_db_prep_save(self, value, add)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.TimeField]
+
+class URLField(Field):
+ def __init__(self, name, verbose_name, verify_exists=True, **kwargs):
+ if verify_exists:
+ kwargs.setdefault('validator_list', []).append(validators.isExistingURL)
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ return [formfields.URLField]
+
+class USStateField(Field):
+ def get_manipulator_field_objs(self):
+ return [formfields.USStateField]
+
+class XMLField(Field):
+ def __init__(self, name, verbose_name, schema_path, **kwargs):
+ self.schema_path = schema_path
+ Field.__init__(self, name, verbose_name, **kwargs)
+
+ 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, rel_name=None, **kwargs):
+ try:
+ to_name = to._meta.object_name.lower()
+ except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
+ kwargs['name'] = kwargs['name']
+ kwargs['verbose_name'] = kwargs['verbose_name']
+ else:
+ to_field = to_field or to._meta.pk.name
+ kwargs['name'] = kwargs.get('name', to_name + '_id')
+ kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name)
+ rel_name = rel_name or to_name
+ kwargs['rel'] = ManyToOne(to, rel_name, to_field,
+ num_in_admin=kwargs.pop('num_in_admin', 0),
+ 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),
+ edit_inline_type=kwargs.pop('edit_inline_type', STACKED),
+ 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):
+ return [formfields.IntegerField]
+
+class ManyToManyField(Field):
+ def __init__(self, to, **kwargs):
+ kwargs['name'] = kwargs.get('name', to._meta.module_name)
+ kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name_plural)
+ kwargs['rel'] = ManyToMany(to, to._meta.object_name.lower() + '_id',
+ num_in_admin=kwargs.pop('num_in_admin', 0),
+ related_name=kwargs.pop('related_name', None),
+ filter_interface=kwargs.pop('filter_interface', None),
+ get_choices_from=kwargs.pop('get_choices_from', None),
+ limit_choices_to=kwargs.pop('limit_choices_to', None))
+ Field.__init__(self, **kwargs)
+
+ def get_manipulator_field_objs(self):
+ choices = self.get_choices(include_blank=False)
+ return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]
+
+####################
+# RELATIONSHIPS #
+####################
+
+class ManyToOne:
+ def __init__(self, to, name, field_name, num_in_admin=0, min_num_in_admin=None,
+ max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, edit_inline_type=STACKED,
+ 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.name, self.field_name = 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 = num_extra_on_change
+ self.edit_inline_type, self.related_name = edit_inline_type, 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_cache_name(self):
+ return '_%s_cache' % self.name
+
+ 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 ManyToMany:
+ def __init__(self, to, name, num_in_admin=0, related_name=None,
+ filter_interface=None, get_choices_from=None, limit_choices_to=None):
+ self.to, self.name = to._meta, name
+ self.num_in_admin = num_in_admin
+ self.related_name = related_name
+ self.filter_interface, self.get_choices_from = filter_interface, get_choices_from
+ self.limit_choices_to = limit_choices_to or {}
+ self.edit_inline = False
+
+class OneToOne(ManyToOne):
+ def __init__(self, to, name, field_name, num_in_admin=0, edit_inline=False,
+ edit_inline_type=STACKED, related_name=None, limit_choices_to=None, lookup_overrides=None,
+ raw_id_admin=False):
+ self.to, self.name, self.field_name = to._meta, name, field_name
+ self.num_in_admin, self.edit_inline = num_in_admin, edit_inline
+ self.edit_inline_type, self.related_name = edit_inline_type, related_name
+ self.limit_choices_to = limit_choices_to or {}
+ self.lookup_overrides = lookup_overrides or {}
+ self.raw_id_admin = raw_id_admin
+
+class Admin:
+ def __init__(self, fields, js=None, list_display=None, list_filter=None, date_hierarchy=None,
+ save_as=False, ordering=None, search_fields=None, save_on_top=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
+
+ def copy(self):
+ return copy.deepcopy(self)
diff --git a/django/core/paginator.py b/django/core/paginator.py
new file mode 100644
index 0000000000..6e6d4431b0
--- /dev/null
+++ b/django/core/paginator.py
@@ -0,0 +1,76 @@
+from copy import copy
+from math import ceil
+
+class InvalidPage(Exception):
+ pass
+
+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
+ 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
+ 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
+
+ def get_page(self, page_number):
+ try:
+ page_number = int(page_number)
+ except ValueError:
+ 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)
+ 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]
+
+ def has_next_page(self, page_number):
+ "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 != [])
+ else:
+ self._has_next[page_number] = page_number < (self.pages - 1)
+ return self._has_next[page_number]
+
+ def has_previous_page(self, page_number):
+ return page_number > 0
+
+ def _get_hits(self):
+ if self._hits is None:
+ order_args = copy(self.args)
+ if order_args.has_key('ordering_tuple'):
+ del order_args['ordering_tuple']
+ if order_args.has_key('select_related'):
+ del order_args['select_related']
+ self._hits = getattr(self.module, self.count_method)(**order_args)
+ return self._hits
+
+ def _get_pages(self):
+ if self._pages is None:
+ self._pages = int(ceil(self.hits / float(self.num_per_page)))
+ return self._pages
+
+ hits = property(_get_hits)
+ pages = property(_get_pages)
diff --git a/django/core/rss.py b/django/core/rss.py
new file mode 100644
index 0000000000..a381a9cd78
--- /dev/null
+++ b/django/core/rss.py
@@ -0,0 +1,136 @@
+from django.core import template_loader
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.template import Context
+from django.models.core import sites
+from django.utils import feedgenerator
+from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE
+
+class FeedConfiguration:
+ def __init__(self, slug, title_cb, link_cb, description_cb, get_list_func_cb, get_list_kwargs,
+ param_func=None, param_kwargs_cb=None, get_list_kwargs_cb=None,
+ enc_url=None, enc_length=None, enc_mime_type=None):
+ """
+ slug -- Normal Python string. Used to register the feed.
+
+ title_cb, link_cb, description_cb -- Functions that take the param
+ (if applicable) and return a normal Python string.
+
+ get_list_func_cb -- Function that takes the param and returns a
+ function to use in retrieving items.
+
+ get_list_kwargs -- Dictionary of kwargs to pass to the function
+ returned by get_list_func_cb.
+
+ param_func -- Function to use in retrieving the param (if applicable).
+
+ param_kwargs_cb -- Function that takes the slug and returns a
+ dictionary of kwargs to use in param_func.
+
+ get_list_kwargs_cb -- Function that takes the param and returns a
+ dictionary to use in addition to get_list_kwargs (if applicable).
+
+ The three enc_* parameters are strings representing methods or
+ attributes to call on a particular item to get its enclosure
+ information. Each of those methods/attributes should return a normal
+ Python string.
+ """
+ self.slug = slug
+ self.title_cb, self.link_cb = title_cb, link_cb
+ self.description_cb = description_cb
+ self.get_list_func_cb = get_list_func_cb
+ self.get_list_kwargs = get_list_kwargs
+ self.param_func, self.param_kwargs_cb = param_func, param_kwargs_cb
+ self.get_list_kwargs_cb = get_list_kwargs_cb
+ assert (None == enc_url == enc_length == enc_mime_type) or (enc_url is not None and enc_length is not None and enc_mime_type is not None)
+ self.enc_url = enc_url
+ self.enc_length = enc_length
+ self.enc_mime_type = enc_mime_type
+
+ def get_feed(self, param_slug=None):
+ """
+ Returns a utils.feedgenerator.DefaultRssFeed object, fully populated,
+ representing this FeedConfiguration.
+ """
+ if param_slug:
+ try:
+ param = self.param_func(**self.param_kwargs_cb(param_slug))
+ except ObjectDoesNotExist:
+ raise FeedIsNotRegistered
+ else:
+ param = None
+ current_site = sites.get_current()
+ f = self._get_feed_generator_object(param)
+ title_template = template_loader.get_template('rss/%s_title' % self.slug)
+ description_template = template_loader.get_template('rss/%s_description' % self.slug)
+ kwargs = self.get_list_kwargs.copy()
+ if param and self.get_list_kwargs_cb:
+ kwargs.update(self.get_list_kwargs_cb(param))
+ get_list_func = self.get_list_func_cb(param)
+ for obj in get_list_func(**kwargs):
+ link = obj.get_absolute_url()
+ if not link.startswith('http://'):
+ link = u'http://%s%s' % (current_site.domain, link)
+ enc = None
+ if self.enc_url:
+ enc_url = getattr(obj, self.enc_url)
+ enc_length = getattr(obj, self.enc_length)
+ enc_mime_type = getattr(obj, self.enc_mime_type)
+ try:
+ enc_url = enc_url()
+ except TypeError:
+ pass
+ try:
+ enc_length = enc_length()
+ except TypeError:
+ pass
+ try:
+ enc_mime_type = enc_mime_type()
+ except TypeError:
+ pass
+ enc = feedgenerator.Enclosure(enc_url.decode('utf-8'),
+ (enc_length and str(enc_length).decode('utf-8') or ''), enc_mime_type.decode('utf-8'))
+ f.add_item(
+ title = title_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'),
+ link = link,
+ description = description_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'),
+ unique_id=link,
+ enclosure=enc,
+ )
+ return f
+
+ def _get_feed_generator_object(self, param):
+ current_site = sites.get_current()
+ link = self.link_cb(param).decode()
+ if not link.startswith('http://'):
+ link = u'http://%s%s' % (current_site.domain, link)
+ return feedgenerator.DefaultRssFeed(
+ title = self.title_cb(param).decode(),
+ link = link,
+ description = self.description_cb(param).decode(),
+ language = LANGUAGE_CODE.decode(),
+ )
+
+
+# global dict used by register_feed and get_registered_feed
+_registered_feeds = {}
+
+class FeedIsNotRegistered(Exception):
+ pass
+
+class FeedRequiresParam(Exception):
+ pass
+
+def register_feed(feed):
+ _registered_feeds[feed.slug] = feed
+
+def get_registered_feed(slug):
+ # try to load a RSS settings module so that feeds can be registered
+ try:
+ __import__(SETTINGS_MODULE + '_rss', '', '', [''])
+ except (KeyError, ImportError, ValueError):
+ pass
+ try:
+ return _registered_feeds[slug]
+ except KeyError:
+ raise FeedIsNotRegistered
+
diff --git a/django/core/template.py b/django/core/template.py
new file mode 100644
index 0000000000..3c1ebb8396
--- /dev/null
+++ b/django/core/template.py
@@ -0,0 +1,488 @@
+"""
+This is the CMS common templating system, shared among all CMS modules that
+require control over output.
+
+How it works:
+
+The tokenize() function converts a template string (i.e., a string containing
+markup with custom template tags) to tokens, which can be either plain text
+(TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK).
+
+The Parser() class takes a list of tokens in its constructor, and its parse()
+method returns a compiled template -- which is, under the hood, a list of
+Node objects.
+
+Each Node is responsible for creating some sort of output -- e.g. simple text
+(TextNode), variable values in a given context (VariableNode), results of basic
+logic (IfNode), results of looping (ForNode), or anything else. The core Node
+types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can
+define their own custom node types.
+
+Each Node has a render() method, which takes a Context and returns a string of
+the rendered node. For example, the render() method of a Variable Node returns
+the variable's value as a string. The render() method of an IfNode returns the
+rendered output of whatever was inside the loop, recursively.
+
+The Template class is a convenient wrapper that takes care of template
+compilation and rendering.
+
+Usage:
+
+The only thing you should ever use directly in this file is the Template class.
+Create a compiled template object with a template_string, then call render()
+with a context. In the compilation stage, the TemplateSyntaxError exception
+will be raised if the template doesn't have proper syntax.
+
+Sample code:
+
+>>> import template
+>>> s = '''
+... <html>
+... {% if test %}
+... <h1>{{ varvalue }}</h1>
+... {% endif %}
+... </html>
+... '''
+>>> t = template.Template(s)
+
+(t is now a compiled template, and its render() method can be called multiple
+times with multiple contexts)
+
+>>> c = template.Context({'test':True, 'varvalue': 'Hello'})
+>>> t.render(c)
+'\n<html>\n\n <h1>Hello</h1>\n\n</html>\n'
+>>> c = template.Context({'test':False, 'varvalue': 'Hello'})
+>>> t.render(c)
+'\n<html>\n\n</html>\n'
+"""
+import re
+
+__all__ = ('Template','Context','compile_string')
+
+TOKEN_TEXT = 0
+TOKEN_VAR = 1
+TOKEN_BLOCK = 2
+
+# template syntax constants
+FILTER_SEPARATOR = '|'
+FILTER_ARGUMENT_SEPARATOR = ':'
+VARIABLE_ATTRIBUTE_SEPARATOR = '.'
+BLOCK_TAG_START = '{%'
+BLOCK_TAG_END = '%}'
+VARIABLE_TAG_START = '{{'
+VARIABLE_TAG_END = '}}'
+
+ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'
+
+# match a variable or block tag and capture the entire tag, including start/end delimiters
+tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
+ re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))
+
+# global dict used by register_tag; maps custom tags to callback functions
+registered_tags = {}
+
+# global dict used by register_filter; maps custom filters to callback functions
+registered_filters = {}
+
+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 Template:
+ def __init__(self, template_string):
+ "Compilation stage"
+ self.nodelist = compile_string(template_string)
+
+ def __iter__(self):
+ for node in self.nodelist:
+ for subnode in node:
+ yield subnode
+
+ def render(self, context):
+ "Display stage -- can be called many times"
+ return self.nodelist.render(context)
+
+def compile_string(template_string):
+ "Compiles template_string into NodeList ready for rendering"
+ tokens = tokenize(template_string)
+ parser = Parser(tokens)
+ return parser.parse()
+
+class Context:
+ "A stack container for variable context"
+ def __init__(self, dict={}):
+ 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 ''
+
+ 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 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"
+ self.token_type, self.contents = token_type, contents
+
+ def __str__(self):
+ return '<%s token: "%s...">' % (
+ {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type],
+ self.contents[:20].replace('\n', '')
+ )
+
+def tokenize(template_string):
+ "Return a list of tokens from a given template_string"
+ # remove all empty strings, because the regex has a tendency to add them
+ bits = filter(None, tag_re.split(template_string))
+ return map(create_token, bits)
+
+def create_token(token_string):
+ "Convert the given token string into a new Token object and return it"
+ if token_string.startswith(VARIABLE_TAG_START):
+ return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
+ elif token_string.startswith(BLOCK_TAG_START):
+ return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
+ else:
+ return Token(TOKEN_TEXT, token_string)
+
+class Parser:
+ def __init__(self, tokens):
+ self.tokens = tokens
+
+ def parse(self, parse_until=[]):
+ nodelist = NodeList()
+ while self.tokens:
+ token = self.next_token()
+ if token.token_type == TOKEN_TEXT:
+ nodelist.append(TextNode(token.contents))
+ elif token.token_type == TOKEN_VAR:
+ if not token.contents:
+ raise TemplateSyntaxError, "Empty variable tag"
+ nodelist.append(VariableNode(token.contents))
+ elif token.token_type == TOKEN_BLOCK:
+ if token.contents in parse_until:
+ # put token back on token list so calling code knows why it terminated
+ self.prepend_token(token)
+ return nodelist
+ try:
+ command = token.contents.split()[0]
+ except IndexError:
+ raise TemplateSyntaxError, "Empty block tag"
+ try:
+ # execute callback function for this tag and append resulting node
+ nodelist.append(registered_tags[command](self, token))
+ except KeyError:
+ raise TemplateSyntaxError, "Invalid block tag: '%s'" % command
+ if parse_until:
+ raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until)
+ return nodelist
+
+ def next_token(self):
+ return self.tokens.pop(0)
+
+ def prepend_token(self, token):
+ self.tokens.insert(0, token)
+
+ def delete_first_token(self):
+ del self.tokens[0]
+
+class FilterParser:
+ """Parse a variable token and its optional filters (all as a single string),
+ and return a list of tuples of the filter name and arguments.
+ Sample:
+ >>> token = 'variable|default:"Default value"|date:"Y-m-d"'
+ >>> p = FilterParser(token)
+ >>> p.filters
+ [('default', 'Default value'), ('date', 'Y-m-d')]
+ >>> p.var
+ 'variable'
+
+ This class should never be instantiated outside of the
+ get_filters_from_token helper function.
+ """
+ def __init__(self, s):
+ self.s = s
+ self.i = -1
+ self.current = ''
+ self.filters = []
+ self.current_filter_name = None
+ self.current_filter_arg = None
+ # First read the variable part
+ self.var = self.read_alphanumeric_token()
+ if not self.var:
+ raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s
+ if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_':
+ raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var
+ # Have we reached the end?
+ if self.current is None:
+ return
+ if self.current != FILTER_SEPARATOR:
+ raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current)
+ # We have a filter separator; start reading the filters
+ self.read_filters()
+
+ def next_char(self):
+ self.i = self.i + 1
+ try:
+ self.current = self.s[self.i]
+ except IndexError:
+ self.current = None
+
+ def read_alphanumeric_token(self):
+ """Read a variable name or filter name, which are continuous strings of
+ alphanumeric characters + the underscore"""
+ var = ''
+ while 1:
+ self.next_char()
+ if self.current is None:
+ break
+ if self.current not in ALLOWED_VARIABLE_CHARS:
+ break
+ var += self.current
+ return var
+
+ def read_filters(self):
+ while 1:
+ filter_name, arg = self.read_filter()
+ if not registered_filters.has_key(filter_name):
+ raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name
+ if registered_filters[filter_name][1] == True and arg is None:
+ raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name
+ if registered_filters[filter_name][1] == False and arg is not None:
+ raise TemplateSyntaxError, "Filter '%s' should not have an argument" % filter_name
+ self.filters.append((filter_name, arg))
+ if self.current is None:
+ break
+
+ def read_filter(self):
+ self.current_filter_name = self.read_alphanumeric_token()
+ # Have we reached the end?
+ if self.current is None:
+ return (self.current_filter_name, None)
+ # Does the filter have an argument?
+ if self.current == FILTER_ARGUMENT_SEPARATOR:
+ self.current_filter_arg = self.read_arg()
+ return (self.current_filter_name, self.current_filter_arg)
+ # Next thing MUST be a pipe
+ if self.current != FILTER_SEPARATOR:
+ raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current)
+ return (self.current_filter_name, self.current_filter_arg)
+
+ def read_arg(self):
+ # First read a "
+ self.next_char()
+ if self.current != '"':
+ raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current
+ self.escaped = False
+ arg = ''
+ while 1:
+ self.next_char()
+ if self.current == '"' and not self.escaped:
+ break
+ if self.current == '\\' and not self.escaped:
+ self.escaped = True
+ continue
+ if self.current == '\\' and self.escaped:
+ arg += '\\'
+ self.escaped = False
+ continue
+ if self.current == '"' and self.escaped:
+ arg += '"'
+ self.escaped = False
+ continue
+ if self.escaped and self.current not in '\\"':
+ raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s
+ if self.current is None:
+ raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s
+ arg += self.current
+ # self.current must now be '"'
+ self.next_char()
+ return arg
+
+def get_filters_from_token(token):
+ "Convenient wrapper for FilterParser"
+ p = FilterParser(token)
+ return (p.var, p.filters)
+
+def resolve_variable(path, context):
+ """
+ Returns the resolved variable, which may contain attribute syntax, within
+ the given context.
+
+ >>> c = {'article': {'section':'News'}}
+ >>> resolve_variable('article.section', c)
+ 'News'
+ >>> resolve_variable('article', c)
+ {'section': 'News'}
+ >>> class AClass: pass
+ >>> c = AClass()
+ >>> c.article = AClass()
+ >>> c.article.section = 'News'
+ >>> resolve_variable('article.section', c)
+ 'News'
+
+ (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
+ """
+ current = context
+ bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR)
+ while bits:
+ try: # dictionary lookup
+ current = current[bits[0]]
+ except (TypeError, AttributeError, KeyError):
+ try: # attribute lookup
+ current = getattr(current, bits[0])
+ if callable(current):
+ if getattr(current, 'alters_data', False):
+ current = ''
+ else:
+ try: # method call (assuming no args required)
+ current = current()
+ except SilentVariableFailure:
+ current = ''
+ except TypeError: # arguments *were* required
+ current = '' # invalid method call
+ 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
+ del bits[0]
+ return current
+
+def resolve_variable_with_filters(var_string, context):
+ """
+ var_string is a full variable expression with optional filters, like:
+ a.b.c|lower|date:"y/m/d"
+ This function resolves the variable in the context, applies all filters and
+ returns the object.
+ """
+ var, filters = get_filters_from_token(var_string)
+ try:
+ obj = resolve_variable(var, context)
+ except VariableDoesNotExist:
+ obj = ''
+ for name, arg in filters:
+ obj = registered_filters[name][0](obj, arg)
+ return obj
+
+class Node:
+ def render(self, context):
+ "Return the node rendered as a string"
+ pass
+
+ def __iter__(self):
+ yield self
+
+ def get_nodes_by_type(self, nodetype):
+ "Return a list of all nodes (within this node and its nodelist) of the given type"
+ nodes = []
+ if isinstance(self, nodetype):
+ nodes.append(self)
+ if hasattr(self, 'nodelist'):
+ nodes.extend(self.nodelist.get_nodes_by_type(nodetype))
+ return nodes
+
+class NodeList(list):
+ def render(self, context):
+ bits = []
+ for node in self:
+ if isinstance(node, Node):
+ bits.append(node.render(context))
+ else:
+ bits.append(node)
+ return ''.join(bits)
+
+ def get_nodes_by_type(self, nodetype):
+ "Return a list of all nodes of the given type"
+ nodes = []
+ for node in self:
+ nodes.extend(node.get_nodes_by_type(nodetype))
+ return nodes
+
+class TextNode(Node):
+ def __init__(self, s):
+ self.s = s
+
+ def __repr__(self):
+ return "<Text Node: '%s'>" % self.s[:25]
+
+ def render(self, context):
+ return self.s
+
+class VariableNode(Node):
+ def __init__(self, var_string):
+ self.var_string = var_string
+
+ def __repr__(self):
+ return "<Variable Node: %s>" % self.var_string
+
+ def render(self, context):
+ output = resolve_variable_with_filters(self.var_string, context)
+ # Check type so that we don't run str() on a Unicode object
+ if not isinstance(output, basestring):
+ output = str(output)
+ elif isinstance(output, unicode):
+ output = output.encode('utf-8')
+ return output
+
+def register_tag(token_command, callback_function):
+ registered_tags[token_command] = callback_function
+
+def unregister_tag(token_command):
+ del registered_tags[token_command]
+
+def register_filter(filter_name, callback_function, has_arg):
+ registered_filters[filter_name] = (callback_function, has_arg)
+
+def unregister_filter(filter_name):
+ del registered_filters[filter_name]
+
+import defaulttags
+import defaultfilters
diff --git a/django/core/template_file.py b/django/core/template_file.py
new file mode 100644
index 0000000000..71139595ab
--- /dev/null
+++ b/django/core/template_file.py
@@ -0,0 +1,18 @@
+"Wrapper for loading templates from files"
+from django.conf.settings import TEMPLATE_DIRS
+from template import TemplateDoesNotExist
+import os
+
+TEMPLATE_FILE_EXTENSION = '.html'
+
+def load_template_source(template_name, template_dirs=None):
+ if not template_dirs:
+ template_dirs = TEMPLATE_DIRS
+ tried = []
+ for template_dir in template_dirs:
+ filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION
+ try:
+ return open(filepath).read()
+ except IOError:
+ tried.append(filepath)
+ raise TemplateDoesNotExist, str(tried)
diff --git a/django/core/template_loader.py b/django/core/template_loader.py
new file mode 100644
index 0000000000..2e2a098a86
--- /dev/null
+++ b/django/core/template_loader.py
@@ -0,0 +1,142 @@
+"Wrapper for loading templates from storage of some sort (e.g. files or db)"
+import template
+from template_file import load_template_source
+
+class ExtendsError(Exception):
+ pass
+
+def get_template(template_name):
+ """
+ Returns a compiled template.Template object for the given template name,
+ handling template inheritance recursively.
+ """
+ return get_template_from_string(load_template_source(template_name))
+
+def get_template_from_string(source):
+ """
+ Returns a compiled template.Template object for the given template code,
+ handling template inheritance recursively.
+ """
+ return template.Template(source)
+
+def select_template(template_name_list):
+ "Given a list of template names, returns the first that can be loaded."
+ for template_name in template_name_list:
+ try:
+ return get_template(template_name)
+ except template.TemplateDoesNotExist:
+ continue
+ # If we get here, none of the templates could be loaded
+ raise template.TemplateDoesNotExist, ', '.join(template_name_list)
+
+class SuperBlock:
+ "This implements the ability for {{ block.super }} to render the parent block's contents"
+ def __init__(self, context, nodelist):
+ self.context, self.nodelist = context, nodelist
+
+ def super(self):
+ if self.nodelist:
+ return self.nodelist.render(self.context)
+ else:
+ return ''
+
+class BlockNode(template.Node):
+ def __init__(self, name, nodelist):
+ self.name, self.nodelist = name, nodelist
+
+ def __repr__(self):
+ return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
+
+ def render(self, context):
+ context.push()
+ nodelist = hasattr(self, 'original_node_list') and self.original_node_list or None
+ context['block'] = SuperBlock(context, nodelist)
+ result = self.nodelist.render(context)
+ context.pop()
+ return result
+
+class ExtendsNode(template.Node):
+ def __init__(self, nodelist, parent_name, parent_name_var, template_dirs=None):
+ self.nodelist = nodelist
+ self.parent_name, self.parent_name_var = parent_name, parent_name_var
+ self.template_dirs = template_dirs
+
+ def get_parent(self, context):
+ if self.parent_name_var:
+ self.parent_name = template.resolve_variable_with_filters(self.parent_name_var, context)
+ parent = self.parent_name
+ if not parent:
+ error_msg = "Invalid template name in 'extends' tag: %r." % parent
+ if self.parent_name_var:
+ error_msg += " Got this from the %r variable." % self.parent_name_var
+ raise template.TemplateSyntaxError, error_msg
+ try:
+ return get_template_from_string(load_template_source(parent, self.template_dirs))
+ except template.TemplateDoesNotExist:
+ raise template.TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
+
+ def render(self, context):
+ compiled_parent = self.get_parent(context)
+ parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode)
+ parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
+ for block_node in self.nodelist.get_nodes_by_type(BlockNode):
+ # Check for a BlockNode with this node's name, and replace it if found.
+ try:
+ parent_block = parent_blocks[block_node.name]
+ except KeyError:
+ # This BlockNode wasn't found in the parent template, but the
+ # parent block might be defined in the parent's *parent*, so we
+ # add this BlockNode to the parent's ExtendsNode nodelist, so
+ # it'll be checked when the parent node's render() is called.
+ if parent_is_child:
+ compiled_parent.nodelist[0].nodelist.append(block_node)
+ else:
+ # Save the original nodelist. It's used by BlockNode.
+ parent_block.original_node_list = parent_block.nodelist
+ parent_block.nodelist = block_node.nodelist
+ return compiled_parent.render(context)
+
+def do_block(parser, token):
+ """
+ Define a block that can be overridden by child templates.
+ """
+ bits = token.contents.split()
+ if len(bits) != 2:
+ raise template.TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0]
+ block_name = bits[1]
+ # Keep track of the names of BlockNodes found in this template, so we can
+ # check for duplication.
+ try:
+ if block_name in parser.__loaded_blocks:
+ raise template.TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name)
+ parser.__loaded_blocks.append(block_name)
+ except AttributeError: # parser._loaded_blocks isn't a list yet
+ parser.__loaded_blocks = [block_name]
+ nodelist = parser.parse(('endblock',))
+ parser.delete_first_token()
+ return BlockNode(block_name, nodelist)
+
+def do_extends(parser, token):
+ """
+ Signal that this template extends a parent template.
+
+ This tag may be used in two ways: ``{% extends "base" %}`` (with quotes)
+ uses the literal value "base" as the name of the parent template to extend,
+ or ``{% entends variable %}`` uses the value of ``variable`` as the name
+ of the parent template to extend.
+ """
+ bits = token.contents.split()
+ if len(bits) != 2:
+ raise template.TemplateSyntaxError, "'%s' takes one argument" % bits[0]
+ parent_name, parent_name_var = None, None
+ if (bits[1].startswith('"') and bits[1].endswith('"')) or (bits[1].startswith("'") and bits[1].endswith("'")):
+ parent_name = bits[1][1:-1]
+ else:
+ parent_name_var = bits[1]
+ nodelist = parser.parse()
+ if nodelist.get_nodes_by_type(ExtendsNode):
+ raise template.TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0]
+ return ExtendsNode(nodelist, parent_name, parent_name_var)
+
+template.register_tag('block', do_block)
+template.register_tag('extends', do_extends)
diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
new file mode 100644
index 0000000000..469790ddd9
--- /dev/null
+++ b/django/core/urlresolvers.py
@@ -0,0 +1,96 @@
+"""
+This module converts requested URLs to callback view functions.
+
+RegexURLResolver is the main class here. Its resolve() method takes a URL (as
+a string) and returns a tuple in this format:
+
+ (view_function, dict_of_view_function_args)
+"""
+
+from django.core.exceptions import Http404, ViewDoesNotExist
+import re
+
+def get_mod_func(callback):
+ # Converts 'django.views.news.stories.story_detail' to
+ # ['django.views.news.stories', 'story_detail']
+ dot = callback.rindex('.')
+ return callback[:dot], callback[dot+1:]
+
+class RegexURLPattern:
+ def __init__(self, regex, callback, default_args=None):
+ self.regex = re.compile(regex)
+ # callback is something like 'foo.views.news.stories.story_detail',
+ # which represents the path to a module and a view function name.
+ self.callback = callback
+ self.default_args = default_args or {}
+
+ def search(self, path):
+ match = self.regex.search(path)
+ if match:
+ args = dict(match.groupdict(), **self.default_args)
+ try: # Lazily load self.func.
+ return self.func, args
+ except AttributeError:
+ self.func = self.get_callback()
+ return self.func, args
+
+ def get_callback(self):
+ mod_name, func_name = get_mod_func(self.callback)
+ try:
+ return getattr(__import__(mod_name, '', '', ['']), func_name)
+ except (ImportError, AttributeError):
+ raise ViewDoesNotExist, self.callback
+
+class RegexURLMultiplePattern:
+ def __init__(self, regex, urlconf_module):
+ self.regex = re.compile(regex)
+ # urlconf_module is a string representing the module containing urlconfs.
+ self.urlconf_module = urlconf_module
+
+ def search(self, path):
+ match = self.regex.search(path)
+ if match:
+ new_path = path[match.end():]
+ try: # Lazily load self.url_patterns.
+ self.url_patterns
+ except AttributeError:
+ self.url_patterns = self.get_url_patterns()
+ for pattern in self.url_patterns:
+ sub_match = pattern.search(new_path)
+ if sub_match:
+ return sub_match
+
+ def get_url_patterns(self):
+ return __import__(self.urlconf_module, '', '', ['']).urlpatterns
+
+class RegexURLResolver:
+ def __init__(self, url_patterns):
+ # url_patterns is a list of RegexURLPattern or RegexURLMultiplePattern objects.
+ self.url_patterns = url_patterns
+
+ def resolve(self, app_path):
+ # app_path is the full requested Web path. This is assumed to have a
+ # leading slash but doesn't necessarily have a trailing slash.
+ # Examples:
+ # "/news/2005/may/"
+ # "/news/"
+ # "/polls/latest"
+ # A home (root) page is represented by "/".
+ app_path = app_path[1:] # Trim leading slash.
+ for pattern in self.url_patterns:
+ match = pattern.search(app_path)
+ if match:
+ return match
+ # None of the regexes matched, so raise a 404.
+ raise Http404, app_path
+
+class Error404Resolver:
+ def __init__(self, callback):
+ self.callback = callback
+
+ def resolve(self):
+ mod_name, func_name = get_mod_func(self.callback)
+ try:
+ return getattr(__import__(mod_name, '', '', ['']), func_name), {}
+ except (ImportError, AttributeError):
+ raise ViewDoesNotExist, self.callback
diff --git a/django/core/validators.py b/django/core/validators.py
new file mode 100644
index 0000000000..c92d86e1d6
--- /dev/null
+++ b/django/core/validators.py
@@ -0,0 +1,420 @@
+"""
+A library of validators that return None and raise ValidationError when the
+provided data isn't valid.
+
+Validators may be callable classes, and they may have an 'always_test'
+attribute. If an 'always_test' attribute exists (regardless of value), the
+validator will *always* be run, regardless of whether its associated
+form field is required.
+"""
+
+import re
+
+_datere = r'\d{4}-((?:0?[1-9])|(?:1[0-2]))-((?:0?[1-9])|(?:[12][0-9])|(?:3[0-1]))'
+_timere = r'(?:[01]?[0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?'
+alnum_re = re.compile(r'^\w+$')
+alnumurl_re = re.compile(r'^[\w/]+$')
+ansi_date_re = re.compile('^%s$' % _datere)
+ansi_time_re = re.compile('^%s$' % _timere)
+ansi_datetime_re = re.compile('^%s %s$' % (_datere, _timere))
+email_re = re.compile(r'^[-\w.+]+@\w[\w.-]+$')
+integer_re = re.compile(r'^-?\d+$')
+phone_re = re.compile(r'^[A-PR-Y0-9]{3}-[A-PR-Y0-9]{3}-[A-PR-Y0-9]{4}$', re.IGNORECASE)
+url_re = re.compile(r'^http://\S+$')
+
+JING = '/usr/bin/jing'
+
+class ValidationError(Exception):
+ def __init__(self, message):
+ "ValidationError can be passed a string or a list."
+ if isinstance(message, list):
+ self.messages = message
+ else:
+ assert isinstance(message, basestring), ("%s should be a string" % repr(message))
+ self.messages = [message]
+ def __str__(self):
+ # This is needed because, without a __str__(), printing an exception
+ # instance would result in this:
+ # AttributeError: ValidationError instance has no attribute 'args'
+ # See http://www.python.org/doc/current/tut/node10.html#handling
+ return str(self.messages)
+
+class CriticalValidationError(Exception):
+ def __init__(self, message):
+ "ValidationError can be passed a string or a list."
+ if isinstance(message, list):
+ self.messages = message
+ else:
+ assert isinstance(message, basestring), ("'%s' should be a string" % message)
+ self.messages = [message]
+ def __str__(self):
+ return str(self.messages)
+
+def isAlphaNumeric(field_data, all_data):
+ if not alnum_re.search(field_data):
+ raise ValidationError, "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 and slashes."
+
+def isLowerCase(field_data, all_data):
+ if field_data.lower() != field_data:
+ raise ValidationError, "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."
+
+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."
+
+def isCommaSeparatedEmailList(field_data, all_data):
+ """
+ Checks that field_data is a string of e-mail addresses separated by commas.
+ Blank field_data values will not throw a validation error, and whitespace
+ is allowed around the commas.
+ """
+ for supposed_email in field_data.split(','):
+ try:
+ isValidEmail(supposed_email.strip(), '')
+ except ValidationError:
+ raise ValidationError, "Enter valid e-mail addresses separated by commas."
+
+def isNotEmpty(field_data, all_data):
+ if field_data.strip() == '':
+ raise ValidationError, "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."
+
+def isNotOnlyDigits(field_data, all_data):
+ if field_data.isdigit():
+ raise ValidationError, "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."
+
+def isOnlyLetters(field_data, all_data):
+ if not field_data.isalpha():
+ raise ValidationError, "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.'
+
+def isValidANSITime(field_data, all_data):
+ if not ansi_time_re.search(field_data):
+ raise ValidationError, '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.'
+
+def isValidEmail(field_data, all_data):
+ if not email_re.search(field_data):
+ raise ValidationError, 'Enter a valid e-mail address.'
+
+def isValidImage(field_data, all_data):
+ """
+ Checks that the file-upload field data contains a valid image (GIF, JPG,
+ PNG, possibly others -- whatever the Python Imaging Library supports).
+ """
+ from PIL import Image
+ from cStringIO import StringIO
+ 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."
+
+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
+
+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
+
+def isValidQuicktimeVideoURL(field_data, all_data):
+ "Checks that the given URL is a video that can be played by QuickTime (qt, mpeg)"
+ uc = URLMimeTypeCheck(('video/quicktime', 'video/mpeg',))
+ try:
+ uc(field_data, all_data)
+ except URLMimeTypeCheck.InvalidContentType:
+ raise ValidationError, "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."
+
+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)
+
+def isWellFormedXmlFragment(field_data, all_data):
+ isWellFormedXml('<root>%s</root>' % field_data, all_data)
+
+def isExistingURL(field_data, all_data):
+ import urllib2
+ try:
+ u = urllib2.urlopen(field_data)
+ except ValueError:
+ raise ValidationError, "Invalid URL: %s" % field_data
+ except: # urllib2.HTTPError, urllib2.URLError, httplib.InvalidURL, etc.
+ raise ValidationError, "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."
+
+def hasNoProfanities(field_data, all_data):
+ """
+ Checks that the given string has no profanities in it. This does a simple
+ check for whether each profanity exists within the string, so 'fuck' will
+ catch 'motherfucker' as well. Raises a ValidationError such as:
+ Watch your mouth! The words "f--k" and "s--t" are not allowed here.
+ """
+ bad_words = ['asshat', 'asshead', 'asshole', 'cunt', 'fuck', 'gook', 'nigger', 'shit'] # all in lower case
+ field_data = field_data.lower() # normalize
+ words_seen = [w for w in bad_words if field_data.find(w) > -1]
+ if words_seen:
+ from django.utils.text import get_text_list
+ plural = len(words_seen) > 1
+ raise ValidationError, "Watch your mouth! The word%s %s %s not allowed here." % \
+ (plural and 's' or '',
+ get_text_list(['"%s%s%s"' % (i[0], '-'*(len(i)-2), i[-1]) for i in words_seen], 'and'),
+ plural and 'are' or 'is')
+
+class AlwaysMatchesOtherField:
+ def __init__(self, other_field_name, error_message=None):
+ self.other = other_field_name
+ self.error_message = error_message or "This field must match the '%s' field." % self.other
+ self.always_test = True
+
+ def __call__(self, field_data, all_data):
+ if field_data != all_data[self.other]:
+ raise ValidationError, self.error_message
+
+class RequiredIfOtherFieldGiven:
+ def __init__(self, other_field_name, error_message=None):
+ self.other = other_field_name
+ self.error_message = error_message or "Please enter both fields or leave them both empty."
+ self.always_test = True
+
+ def __call__(self, field_data, all_data):
+ if all_data[self.other] and not field_data:
+ raise ValidationError, self.error_message
+
+class RequiredIfOtherFieldNotGiven:
+ def __init__(self, other_field_name, error_message=None):
+ self.other = other_field_name
+ self.error_message = error_message or "Please enter something for at least one field."
+ self.always_test = True
+
+ def __call__(self, field_data, all_data):
+ if not all_data.get(self.other, False) and not field_data:
+ raise ValidationError, self.error_message
+
+class RequiredIfOtherFieldsGiven:
+ "Like RequiredIfOtherFieldGiven, but takes a list of required field names instead of a single field name"
+ def __init__(self, other_field_names, error_message=None):
+ self.other = other_field_names
+ self.error_message = error_message or "Please enter both fields or leave them both empty."
+ self.always_test = True
+
+ def __call__(self, field_data, all_data):
+ for field in self.other:
+ if all_data.has_key(field) and all_data[field] and not field_data:
+ raise ValidationError, self.error_message
+
+class RequiredIfOtherFieldEquals:
+ def __init__(self, other_field, other_value, error_message=None):
+ self.other_field = other_field
+ self.other_value = other_value
+ self.error_message = error_message or "This field must be given if %s is %s" % (other_field, other_value)
+ self.always_test = True
+
+ def __call__(self, field_data, all_data):
+ if all_data.has_key(self.other_field) and all_data[self.other_field] == self.other_value and not field_data:
+ raise ValidationError(self.error_message)
+
+class RequiredIfOtherFieldDoesNotEqual:
+ def __init__(self, other_field, other_value, error_message=None):
+ self.other_field = other_field
+ self.other_value = other_value
+ self.error_message = error_message or "This field must be given if %s is not %s" % (other_field, other_value)
+ self.always_test = True
+
+ def __call__(self, field_data, all_data):
+ if all_data.has_key(self.other_field) and all_data[self.other_field] != self.other_value and not field_data:
+ raise ValidationError(self.error_message)
+
+class IsLessThanOtherField:
+ def __init__(self, other_field_name, error_message):
+ self.other, self.error_message = other_field_name, error_message
+
+ def __call__(self, field_data, all_data):
+ if field_data > all_data[self.other]:
+ raise ValidationError, self.error_message
+
+class UniqueAmongstFieldsWithPrefix:
+ def __init__(self, field_name, prefix, error_message):
+ self.field_name, self.prefix = field_name, prefix
+ self.error_message = error_message or "Duplicate values are not allowed."
+
+ def __call__(self, field_data, all_data):
+ for field_name, value in all_data.items():
+ if field_name != self.field_name and value == field_data:
+ raise ValidationError, self.error_message
+
+class IsAPowerOf:
+ """
+ >>> v = IsAPowerOf(2)
+ >>> v(4, None)
+ >>> v(8, None)
+ >>> v(16, None)
+ >>> v(17, None)
+ django.core.validators.ValidationError: ['This value must be a power of 2.']
+ """
+ def __init__(self, power_of):
+ self.power_of = power_of
+
+ def __call__(self, field_data, all_data):
+ 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
+
+class IsValidFloat:
+ def __init__(self, max_digits, decimal_places):
+ self.max_digits, self.decimal_places = max_digits, decimal_places
+
+ def __call__(self, field_data, all_data):
+ data = str(field_data)
+ try:
+ float(data)
+ except ValueError:
+ raise ValidationError, "Please enter a valid decimal number."
+ if len(data) > (self.max_digits + 1):
+ raise ValidationError, "Please enter a valid decimal number with at most %s total digit%s." % \
+ (self.max_digits, self.max_digits > 1 and 's' or '')
+ if '.' in data and len(data.split('.')[1]) > self.decimal_places:
+ raise ValidationError, "Please enter a valid decimal number with at most %s decimal place%s." % \
+ (self.decimal_places, self.decimal_places > 1 and 's' or '')
+
+class HasAllowableSize:
+ """
+ Checks that the file-upload field data is a certain size. min_size and
+ max_size are measurements in bytes.
+ """
+ def __init__(self, min_size=None, max_size=None, min_error_message=None, max_error_message=None):
+ self.min_size, self.max_size = min_size, max_size
+ self.min_error_message = min_error_message or "Make sure your uploaded file is at least %s bytes big." % min_size
+ self.max_error_message = max_error_message or "Make sure your uploaded file is at most %s bytes big." % min_size
+
+ def __call__(self, field_data, all_data):
+ if self.min_size is not None and len(field_data['content']) < self.min_size:
+ raise ValidationError, self.min_error_message
+ if self.max_size is not None and len(field_data['content']) > self.max_size:
+ raise ValidationError, self.max_error_message
+
+class URLMimeTypeCheck:
+ "Checks that the provided URL points to a document with a listed mime type"
+ class CouldNotRetrieve(ValidationError):
+ pass
+ class InvalidContentType(ValidationError):
+ pass
+
+ def __init__(self, mime_type_list):
+ self.mime_type_list = mime_type_list
+
+ def __call__(self, field_data, all_data):
+ import urllib2
+ try:
+ isValidURL(field_data, all_data)
+ except ValidationError:
+ raise
+ try:
+ info = urllib2.urlopen(field_data).info()
+ except (urllib2.HTTPError, urllib2.URLError):
+ raise URLMimeTypeCheck.CouldNotRetrieve, "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 %s returned the invalid Content-Type header '%s'." % (field_data, content_type)
+
+class RelaxNGCompact:
+ "Validate against a Relax NG compact schema"
+ def __init__(self, schema_path, additional_root_element=None):
+ self.schema_path = schema_path
+ self.additional_root_element = additional_root_element
+
+ def __call__(self, field_data, all_data):
+ import os, tempfile
+ if self.additional_root_element:
+ field_data = '<%(are)s>%(data)s\n</%(are)s>' % {
+ 'are': self.additional_root_element,
+ 'data': field_data
+ }
+ filename = tempfile.mktemp() # Insecure, but nothing else worked
+ fp = open(filename, 'w')
+ fp.write(field_data)
+ fp.close()
+ if not os.path.exists(JING):
+ raise Exception, "%s not found!" % JING
+ p = os.popen('%s -c %s %s' % (JING, self.schema_path, filename))
+ errors = [line.strip() for line in p.readlines()]
+ p.close()
+ os.unlink(filename)
+ display_errors = []
+ lines = field_data.split('\n')
+ for error in errors:
+ _, line, level, message = error.split(':', 3)
+ # Scrape the Jing error messages to reword them more nicely.
+ m = re.search(r'Expected "(.*?)" to terminate element starting on line (\d+)', message)
+ if m:
+ display_errors.append('Please close the unclosed %s tag from line %s. (Line starts with "%s".)' % \
+ (m.group(1).replace('/', ''), m.group(2), lines[int(m.group(2)) - 1][:30]))
+ continue
+ if message.strip() == 'text not allowed here':
+ display_errors.append('Some text starting on line %s is not allowed in that context. (Line starts with "%s".)' % \
+ (line, lines[int(line) - 1][:30]))
+ continue
+ m = re.search(r'\s*attribute "(.*?)" not allowed at this point; ignored', message)
+ if m:
+ display_errors.append('"%s" on line %s is an invalid attribute. (Line starts with "%s".)' % \
+ (m.group(1), line, lines[int(line) - 1][:30]))
+ continue
+ m = re.search(r'\s*unknown element "(.*?)"', message)
+ if m:
+ display_errors.append('"<%s>" on line %s is an invalid tag. (Line starts with "%s".)' % \
+ (m.group(1), line, lines[int(line) - 1][:30]))
+ continue
+ if message.strip() == 'required attributes missing':
+ display_errors.append('A tag on line %s is missing one or more required attributes. (Line starts with "%s".)' % \
+ (line, lines[int(line) - 1][:30]))
+ continue
+ m = re.search(r'\s*bad value for attribute "(.*?)"', message)
+ if m:
+ display_errors.append('The "%s" attribute on line %s has an invalid value. (Line starts with "%s".)' % \
+ (m.group(1), line, lines[int(line) - 1][:30]))
+ continue
+ # Failing all those checks, use the default error message.
+ display_error = 'Line %s: %s [%s]' % (line, message, level.strip())
+ display_errors.append(display_error)
+ if len(display_errors) > 0:
+ raise ValidationError, display_errors
diff --git a/django/core/xheaders.py b/django/core/xheaders.py
new file mode 100644
index 0000000000..ec03826ea9
--- /dev/null
+++ b/django/core/xheaders.py
@@ -0,0 +1,22 @@
+"""
+Some pages in our CMS are served up with custom HTTP headers containing useful
+information about those pages -- namely, the contenttype and object ID.
+
+This module contains utility functions for retrieving and doing interesting
+things with these special "X-Headers" (so called because the HTTP spec demands
+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):
+ """
+ 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.
+ """
+ from django.conf.settings import INTERNAL_IPS
+ if request.META['REMOTE_ADDR'] in INTERNAL_IPS:
+ response['X-Object-Type'] = "%s.%s" % (package, python_module_name)
+ response['X-Object-Id'] = str(object_id)
diff --git a/django/middleware/__init__.py b/django/middleware/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/middleware/__init__.py
diff --git a/django/middleware/admin.py b/django/middleware/admin.py
new file mode 100644
index 0000000000..f91b856d3b
--- /dev/null
+++ b/django/middleware/admin.py
@@ -0,0 +1,120 @@
+from django.utils import httpwrappers
+from django.core import template_loader
+from django.core.extensions import CMSContext as Context
+from django.models.auth import sessions, users
+from django.views.registration import passwords
+import base64, md5
+import cPickle as pickle
+
+# secret used in pickled data to guard against tampering
+TAMPER_SECRET = '09VJWE9_RIZZO_j0jwfe09j'
+
+ERROR_MESSAGE = "Please enter a correct username and password. Note that both fields are case-sensitive."
+
+class AdminUserRequired:
+ """
+ Admin middleware. If this is enabled, access to the site will be granted only
+ to valid users with the "is_staff" flag set.
+ """
+
+ def process_view(self, request, view_func, param_dict):
+ """
+ Make sure the user is logged in and is a valid admin user before
+ allowing any access.
+
+ Done at the view point because we need to know if we're running the
+ password reset function.
+ """
+
+ # If this is the password reset view, we don't want to require login
+ # Otherwise the password reset would need its own entry in the httpd
+ # conf, which is a little uglier than this.
+ if view_func == passwords.password_reset or view_func == passwords.password_reset_done:
+ return
+
+ # Check for a logged in, valid user
+ if self.user_is_valid(request.user):
+ return
+
+ # If this isn't alreay the login page, display it
+ if not request.POST.has_key('this_is_the_login_form'):
+ if request.POST:
+ message = "Please log in again, because your session has expired. "\
+ "Don't worry: Your submission has been saved."
+ else:
+ message = ""
+ return self.display_login_form(request, message)
+
+ # Check the password
+ username = request.POST.get('username', '')
+ try:
+ user = users.get_object(username__exact=username)
+ except users.UserDoesNotExist:
+ 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:
+ message = "Usernames cannot contain the '@' character."
+ else:
+ message = "Your e-mail address is not your username. Try '%s' instead." % user.username
+ return self.display_login_form(request, message)
+
+ # The user data is correct; log in the user in and continue
+ else:
+ if self.authenticate_user(user, request.POST.get('password', '')):
+ if request.POST.has_key('post_data'):
+ post_data = decode_post_data(request.POST['post_data'])
+ if post_data and not post_data.has_key('this_is_the_login_form'):
+ # overwrite request.POST with the saved post_data, and continue
+ request.POST = post_data
+ request.user = user
+ request.session = sessions.create_session(user.id)
+ return
+ else:
+ response = httpwrappers.HttpResponseRedirect(request.path)
+ sessions.start_web_session(user.id, request, response)
+ return response
+ else:
+ return self.display_login_form(request, ERROR_MESSAGE)
+
+ def display_login_form(self, request, error_message=''):
+ if request.POST and request.POST.has_key('post_data'):
+ # User has failed login BUT has previously saved 'post_data'
+ post_data = request.POST['post_data']
+ elif request.POST:
+ # User's session must have expired; save their post data
+ post_data = encode_post_data(request.POST)
+ else:
+ post_data = encode_post_data({})
+ t = template_loader.get_template(self.get_login_template_name())
+ c = Context(request, {
+ 'title': 'Log in',
+ 'app_path': request.path,
+ 'post_data': post_data,
+ 'error_message': error_message
+ })
+ return httpwrappers.HttpResponse(t.render(c))
+
+ def authenticate_user(self, user, password):
+ return user.check_password(password) and user.is_staff
+
+ def user_is_valid(self, user):
+ return not user.is_anonymous() and user.is_staff
+
+ def get_login_template_name(self):
+ return "login"
+
+def encode_post_data(post_data):
+ pickled = pickle.dumps(post_data)
+ pickled_md5 = md5.new(pickled + TAMPER_SECRET).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 + TAMPER_SECRET).hexdigest() != tamper_check:
+ from django.core.exceptions import SuspiciousOperation
+ raise SuspiciousOperation, "User may have tampered with session cookie."
+ return pickle.loads(pickled)
diff --git a/django/middleware/common.py b/django/middleware/common.py
new file mode 100644
index 0000000000..aed742925b
--- /dev/null
+++ b/django/middleware/common.py
@@ -0,0 +1,104 @@
+from django.conf import settings
+from django.core import exceptions
+from django.utils import httpwrappers
+from django.core.mail import mail_managers
+from django.views.core.flatfiles import flat_file
+import md5, os
+from urllib import urlencode
+
+class CommonMiddleware:
+ """
+ "Common" middleware for taking care of some basic operations:
+
+ - Forbids access to User-Agents in settings.DISALLOWED_USER_AGENTS
+
+ - URL rewriting: based on the APPEND_SLASH and PREPEND_WWW settings,
+ this middleware will -- shocking, isn't it -- append missing slashes
+ and/or prepend missing "www."s.
+
+ - ETags: if the USE_ETAGS setting is set, ETags will be calculated from
+ the entire page content and Not Modified responses will be returned
+ appropriately.
+
+ - Flat files: for 404 responses, a flat file matching the given path
+ will be looked up and used if found.
+
+ You probably want the CommonMiddleware object to the first entry in your
+ MIDDLEWARE_CLASSES setting;
+ """
+
+ def process_request(self, request):
+ """
+ Check for denied User-Agents and rewrite the URL based on
+ settings.APPEND_SLASH and settings.PREPEND_WWW
+ """
+
+ # Check for denied User-Agents
+ 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>')
+
+ # Check for a redirect based on settings.APPEND_SLASH and settings.PREPEND_WWW
+ old_url = [request.META['HTTP_HOST'], request.path]
+ new_url = old_url[:]
+ if settings.PREPEND_WWW and not old_url[0].startswith('www.'):
+ new_url[0] = 'www.' + old_url[0]
+ # Append a slash if append_slash is set and the URL doesn't have a
+ # trailing slash or a file extension.
+ if settings.APPEND_SLASH and (old_url[1][-1] != '/') and ('.' not in old_url[1].split('/')[-1]):
+ new_url[1] = new_url[1] + '/'
+ if new_url != old_url:
+ # Redirect
+ newurl = "%s://%s%s" % (os.environ.get('HTTPS') == 'on' and 'https' or 'http', new_url[0], new_url[1])
+ if request.GET:
+ newurl += '?' + urlencode(request.GET)
+ return httpwrappers.HttpResponseRedirect(newurl)
+
+ return None
+
+ def process_response(self, request, response):
+ """
+ Check for a flatfile (for 404s) and calculate the Etag, if needed.
+ """
+
+ # If this was a 404, check for a flat file
+ if response.status_code == 404:
+ try:
+ response = flat_file(request, request.path)
+ except exceptions.Http404:
+ # If the referrer was from an internal link or a non-search-engine site,
+ # send a note to the managers.
+ if settings.SEND_BROKEN_LINK_EMAILS:
+ domain = request.META['HTTP_HOST']
+ referer = request.META.get('HTTP_REFERER', None)
+ is_internal = referer and (domain in referer)
+ path = request.get_full_path()
+ if referer and not _is_ignorable_404(path) and (is_internal or '?' not in referer):
+ mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain),
+ "Referrer: %s\nRequested URL: %s\n" % (referer, request.get_full_path()))
+ # If there's no flatfile we want to return the original 404 response
+ return response
+
+ # Use ETags, if requested
+ if settings.USE_ETAGS:
+ etag = md5.new(response.get_content_as_string('utf-8')).hexdigest()
+ if request.META.get('HTTP_IF_NONE_MATCH') == etag:
+ response = httpwrappers.HttpResponseNotModified()
+ else:
+ response['ETag'] = etag
+
+ return response
+
+def _is_ignorable_404(uri):
+ "Returns True if a 404 at the given URL *shouldn't* notify the site managers"
+ for start in settings.IGNORABLE_404_STARTS:
+ if uri.startswith(start):
+ return True
+ for end in settings.IGNORABLE_404_ENDS:
+ if uri.endswith(end):
+ return True
+ if '_files' in uri:
+ # URI is probably from a locally-saved copy of the page.
+ return True
+ return False
diff --git a/django/middleware/doc.py b/django/middleware/doc.py
new file mode 100644
index 0000000000..8f648e0665
--- /dev/null
+++ b/django/middleware/doc.py
@@ -0,0 +1,18 @@
+from django.conf import settings
+from django.utils import httpwrappers
+
+class XViewMiddleware:
+ """
+ Adds an X-View header to internal HEAD requests -- used by the documentation system.
+ """
+
+ def process_view(self, request, view_func, param_dict):
+ """
+ If the request method is HEAD and the IP is internal, quickly return
+ with an x-header indicating the view function. This is used by the
+ documentation module to lookup the view function for an arbitrary page.
+ """
+ if request.META['REQUEST_METHOD'] == 'HEAD' and request.META['REMOTE_ADDR'] in settings.INTERNAL_IPS:
+ response = httpwrappers.HttpResponse()
+ response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__)
+ return response
diff --git a/django/models/__init__.py b/django/models/__init__.py
new file mode 100644
index 0000000000..63cedc9af1
--- /dev/null
+++ b/django/models/__init__.py
@@ -0,0 +1,91 @@
+from django.core import meta
+
+__all__ = ['auth', 'comments', '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 rel_obj, rel_field 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 = rel_obj.get_model_module()
+ rel_obj_name = klass._meta.get_rel_object_method_name(rel_obj, rel_field)
+ if isinstance(rel_field.rel, meta.OneToOne):
+ # Add "get_thingie" methods for one-to-one related objects.
+ # EXAMPLE: Place.get_restaurants_restaurant()
+ func = meta.curry(meta.method_get_related, 'get_object', rel_mod, rel_field)
+ func.__doc__ = "Returns the associated `%s.%s` object." % (rel_obj.app_label, rel_obj.module_name)
+ setattr(klass, 'get_%s' % rel_obj_name, func)
+ elif isinstance(rel_field.rel, meta.ManyToOne):
+ # Add "get_thingie" methods for many-to-one related objects.
+ # EXAMPLE: Poll.get_choice()
+ func = meta.curry(meta.method_get_related, 'get_object', rel_mod, rel_field)
+ func.__doc__ = "Returns the associated `%s.%s` object matching the given criteria." % (rel_obj.app_label, rel_obj.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 = meta.curry(meta.method_get_related, 'get_count', rel_mod, rel_field)
+ func.__doc__ = "Returns the number of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.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 = meta.curry(meta.method_get_related, 'get_list', rel_mod, rel_field)
+ func.__doc__ = "Returns a list of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.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 rel_obj.app_label == klass._meta.app_label:
+ func = meta.curry(meta.method_add_related, rel_obj, rel_mod, rel_field)
+ func.alters_data = True
+ setattr(klass, 'add_%s' % rel_obj_name, func)
+ del func
+ del rel_obj_name, rel_mod, rel_obj, rel_field # clean up
+
+ # Do the same for all related many-to-many objects.
+ for rel_opts, rel_field in klass._meta.get_all_related_many_to_many_objects():
+ rel_mod = rel_opts.get_model_module()
+ rel_obj_name = klass._meta.get_rel_object_method_name(rel_opts, rel_field)
+ setattr(klass, 'get_%s' % rel_obj_name, meta.curry(meta.method_get_related_many_to_many, 'get_object', rel_mod, rel_field))
+ setattr(klass, 'get_%s_count' % rel_obj_name, meta.curry(meta.method_get_related_many_to_many, 'get_count', rel_mod, rel_field))
+ setattr(klass, 'get_%s_list' % rel_obj_name, meta.curry(meta.method_get_related_many_to_many, 'get_list', rel_mod, rel_field))
+ if rel_opts.app_label == klass._meta.app_label:
+ func = meta.curry(meta.method_set_related_many_to_many, rel_opts, rel_field)
+ func.alters_data = True
+ setattr(klass, 'set_%s' % rel_opts.module_name, func)
+ del func
+ del rel_obj_name, rel_mod, rel_opts, rel_field # 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 = meta.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 = meta.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
new file mode 100644
index 0000000000..96e6af9667
--- /dev/null
+++ b/django/models/auth.py
@@ -0,0 +1,290 @@
+from django.core import meta, validators
+from django.models import core
+
+class Permission(meta.Model):
+ fields = (
+ meta.CharField('name', 'name', maxlength=50),
+ meta.ForeignKey(core.Package, name='package'),
+ meta.CharField('codename', 'code name', maxlength=100),
+ )
+ unique_together = (('package', 'codename'),)
+ ordering = (('package', 'ASC'), ('codename', 'ASC'))
+
+ def __repr__(self):
+ return "%s | %s" % (self.package, self.name)
+
+class Group(meta.Model):
+ fields = (
+ meta.CharField('name', 'name', maxlength=80, unique=True),
+ meta.ManyToManyField(Permission, blank=True, filter_interface=meta.HORIZONTAL),
+ )
+ ordering = (('name', 'ASC'),)
+ admin = meta.Admin(
+ fields = (
+ (None, {'fields': ('name', 'permissions')}),
+ ),
+ search_fields = ('name',),
+ )
+
+ def __repr__(self):
+ return self.name
+
+class User(meta.Model):
+ fields = (
+ meta.CharField('username', 'username', maxlength=30, unique=True,
+ validator_list=[validators.isAlphaNumeric]),
+ meta.CharField('first_name', 'first name', maxlength=30, blank=True),
+ meta.CharField('last_name', 'last name', maxlength=30, blank=True),
+ meta.EmailField('email', 'e-mail address', blank=True),
+ meta.CharField('password_md5', 'password', maxlength=32),
+ meta.BooleanField('is_staff', 'staff status',
+ help_text="Designates whether the user can log into this admin site."),
+ meta.BooleanField('is_active', 'active', default=True),
+ meta.BooleanField('is_superuser', 'superuser status'),
+ meta.DateTimeField('last_login', 'last login', default=meta.LazyDate()),
+ meta.DateTimeField('date_joined', 'date joined', default=meta.LazyDate()),
+ 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."),
+ meta.ManyToManyField(Permission, name='user_permissions', blank=True, filter_interface=meta.HORIZONTAL),
+ )
+ ordering = (('username', 'ASC'),)
+ exceptions = ('SiteProfileNotAvailable',)
+ admin = meta.Admin(
+ fields = (
+ (None, {'fields': ('username', 'password_md5')}),
+ ('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 md5
+ self.password_md5 = md5.new(raw_password).hexdigest()
+
+ def check_password(self, raw_password):
+ "Returns a boolean of whether the raw_password was correct."
+ import md5
+ return self.password_md5 == md5.new(raw_password).hexdigest()
+
+ 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()
+ 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])
+ 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, p.codename) for p in self.get_user_permissions()])
+ 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."
+ import md5
+ password_md5 = md5.new(password).hexdigest()
+ now = datetime.datetime.now()
+ user = User(None, username, '', '', email.strip().lower(), password_md5, False, True, False, now, now)
+ 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 whrandom import choice
+ return ''.join([choice(allowed_chars) for i in range(length)])
+
+class Session(meta.Model):
+ fields = (
+ meta.ForeignKey(User),
+ meta.CharField('session_md5', 'session MD5 hash', maxlength=32),
+ meta.DateTimeField('start_time', 'start time', auto_now=True),
+ )
+ module_constants = {
+ # Used for providing pseudo-entropy in creating random session strings.
+ 'SESSION_SALT': 'ijw2f3_MUPPET_avo#*5)(*',
+ # Secret used in cookie to guard against cookie tampering.
+ 'TAMPER_SECRET': 'lj908_PIGGY_j0vajeawej-092j3f',
+ 'TEST_COOKIE_NAME': 'testcookie',
+ 'TEST_COOKIE_VALUE': 'worked',
+ }
+
+ def __repr__(self):
+ return "session started at %s" % self.start_time
+
+ def get_cookie(self):
+ "Returns a tuple of the cookie name and value for this session."
+ import md5
+ from django.conf.settings import AUTH_SESSION_COOKIE
+ return AUTH_SESSION_COOKIE, self.session_md5 + md5.new(self.session_md5 + TAMPER_SECRET).hexdigest()
+
+ def _module_create_session(user_id):
+ "Registers a session and returns the session_md5."
+ import md5, random, sys
+ # The random module is seeded when this Apache child is created.
+ # Use person_id and SESSION_SALT as added salt.
+ session_md5 = md5.new(str(random.randint(user_id, sys.maxint - 1)) + SESSION_SALT).hexdigest()
+ s = Session(None, user_id, session_md5, None)
+ s.save()
+ return s
+
+ def _module_get_session_from_cookie(session_cookie_string):
+ import md5
+ if not session_cookie_string:
+ raise SessionDoesNotExist
+ session_md5, tamper_check = session_cookie_string[:32], session_cookie_string[32:]
+ if md5.new(session_md5 + TAMPER_SECRET).hexdigest() != tamper_check:
+ raise SuspiciousOperation, "User may have tampered with session cookie."
+ return get_object(session_md5__exact=session_md5, select_related=True)
+
+ def _module_destroy_all_sessions(user_id):
+ "Destroys all sessions for a user, logging out all computers."
+ for session in get_list(user_id__exact=user_id):
+ session.delete()
+
+ def _module_start_web_session(user_id, request, response):
+ "Sets the necessary cookie in the given HttpResponse object, also updates last login time for user."
+ from django.models.auth import users
+ from django.conf.settings import REGISTRATION_COOKIE_DOMAIN
+ user = users.get_object(id__exact=user_id)
+ user.last_login = datetime.datetime.now()
+ user.save()
+ session = create_session(user_id)
+ key, value = session.get_cookie()
+ cookie_domain = REGISTRATION_COOKIE_DOMAIN or request.META['SERVER_NAME']
+ response.set_cookie(key, value, domain=cookie_domain)
+
+class Message(meta.Model):
+ fields = (
+ meta.AutoField('id', 'ID', primary_key=True),
+ meta.ForeignKey(User),
+ meta.TextField('message', 'message'),
+ )
+
+ def __repr__(self):
+ return self.message
+
+class LogEntry(meta.Model):
+ module_name = 'log'
+ verbose_name_plural = 'log entries'
+ db_table = 'auth_admin_log'
+ fields = (
+ meta.DateTimeField('action_time', 'action time', auto_now=True),
+ meta.ForeignKey(User),
+ meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type', blank=True, null=True),
+ meta.IntegerField('object_id', 'object ID', blank=True, null=True),
+ meta.CharField('object_repr', 'object representation', maxlength=200),
+ meta.PositiveSmallIntegerField('action_flag', 'action flag'),
+ meta.TextField('change_message', 'change message', blank=True),
+ )
+ ordering = (('action_time', 'DESC'),)
+ 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(id__exact=self.object_id)
+
+ def get_admin_url(self):
+ "Returns the admin URL to edit the object represented by this log entry"
+ return "/%s/%s/%s/" % (self.get_content_type().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/models/comments.py b/django/models/comments.py
new file mode 100644
index 0000000000..0d610c965c
--- /dev/null
+++ b/django/models/comments.py
@@ -0,0 +1,281 @@
+from django.core import meta
+from django.models import auth, core
+
+class Comment(meta.Model):
+ db_table = 'comments'
+ fields = (
+ meta.ForeignKey(auth.User, raw_id_admin=True),
+ meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type'),
+ meta.IntegerField('object_id', 'object ID'),
+ meta.CharField('headline', 'headline', maxlength=255, blank=True),
+ meta.TextField('comment', 'comment', maxlength=3000),
+ meta.PositiveSmallIntegerField('rating1', 'rating #1', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating2', 'rating #2', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating3', 'rating #3', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating4', 'rating #4', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating5', 'rating #5', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating6', 'rating #6', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating7', 'rating #7', blank=True, null=True),
+ meta.PositiveSmallIntegerField('rating8', '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 review on the same thing, but the system
+ # will only use the latest one (with valid_rating=True) in tallying
+ # the reviews.
+ meta.BooleanField('valid_rating', 'is valid rating'),
+ meta.DateTimeField('submit_date', 'date/time submitted', auto_now_add=True),
+ meta.BooleanField('is_public', 'is public'),
+ meta.IPAddressField('ip_address', 'IP address', blank=True, null=True),
+ meta.BooleanField('is_removed', 'is removed',
+ help_text='Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.'),
+ meta.ForeignKey(core.Site),
+ )
+ module_constants = {
+ # used as shared secret between comment form and comment-posting script
+ 'COMMENT_SALT': 'ijw2f3_MRS_PIGGY_LOVES_KERMIT_avo#*5vv0(23j)(*',
+
+ # 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', 'DESC'),)
+ admin = meta.Admin(
+ fields = (
+ (None, {'fields': ('content_type_id', 'object_id', 'site_id')}),
+ ('Content', {'fields': ('user_id', '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_id', 'submit_date', 'content_type_id', '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(id__exact=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 %s at %s\n\n%s\n\nhttp://%s%s' % \
+ (self.get_user().username, self.submit_date,
+ self.comment, self.get_site().domain, 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.
+ """
+ import md5
+ return md5.new(options + photo_options + rating_options + target + COMMENT_SALT).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_groups():
+ if g.id == COMMENTS_MODERATORS_GROUP:
+ return True
+ return False
+
+class FreeComment(meta.Model):
+ "A FreeComment is a comment by a non-registered user"
+ db_table = 'comments_free'
+ fields = (
+ meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type'),
+ meta.IntegerField('object_id', 'object ID'),
+ meta.TextField('comment', 'comment', maxlength=3000),
+ meta.CharField('person_name', "person's name", maxlength=50),
+ meta.DateTimeField('submit_date', 'date/time submitted', auto_now_add=True),
+ meta.BooleanField('is_public', 'is public'),
+ meta.IPAddressField('ip_address', 'IP address'),
+ # TODO: Change this to is_removed, like Comment
+ meta.BooleanField('approved', 'approved by staff'),
+ meta.ForeignKey(core.Site),
+ )
+ ordering = (('submit_date', 'DESC'),)
+ admin = meta.Admin(
+ fields = (
+ (None, {'fields': ('content_type_id', 'object_id', 'site_id')}),
+ ('Content', {'fields': ('person_name', 'comment')}),
+ ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}),
+ ),
+ list_display = ('person_name', 'submit_date', 'content_type_id', '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_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(id__exact=self.object_id)
+ except ObjectDoesNotExist:
+ return None
+
+ get_content_object.short_description = 'Content object'
+
+class KarmaScore(meta.Model):
+ module_name = 'karma'
+ fields = (
+ meta.ForeignKey(auth.User),
+ meta.ForeignKey(Comment),
+ meta.SmallIntegerField('score', 'score', db_index=True),
+ meta.DateTimeField('scored_date', 'date scored', auto_now=True),
+ )
+ unique_together = (('user_id', 'comment_id'),)
+ module_constants = {
+ # what users get if they don't have any karma
+ 'DEFAULT_KARMA': 5,
+ 'KARMA_NEEDED_BEFORE_DISPLAYED': 3,
+ }
+
+ def __repr__(self):
+ return "%d rating by %s" % (self.score, 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):
+ db_table = 'comments_user_flags'
+ fields = (
+ meta.ForeignKey(auth.User),
+ meta.ForeignKey(Comment),
+ meta.DateTimeField('flag_date', 'date flagged', auto_now_add=True),
+ )
+ unique_together = (('user_id', 'comment_id'),)
+
+ 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 %s:\n\n%s' % (user.username, comment.get_as_text())
+ mail_managers('Comment flagged', message, fail_silently=True)
+ f.save()
+
+class ModeratorDeletion(meta.Model):
+ db_table = 'comments_moderator_deletions'
+ fields = (
+ meta.ForeignKey(auth.User, verbose_name='moderator'),
+ meta.ForeignKey(Comment),
+ meta.DateTimeField('deletion_date', 'date deleted', auto_now_add=True),
+ )
+ unique_together = (('user_id', 'comment_id'),)
+
+ def __repr__(self):
+ return "Moderator deletion by %r" % self.get_user()
diff --git a/django/models/core.py b/django/models/core.py
new file mode 100644
index 0000000000..4416f22b76
--- /dev/null
+++ b/django/models/core.py
@@ -0,0 +1,107 @@
+from django.core import meta, validators
+
+class Site(meta.Model):
+ db_table = 'sites'
+ fields = (
+ meta.CharField('domain', 'domain name', maxlength=100),
+ meta.CharField('name', 'display name', maxlength=50),
+ )
+ ordering = (('domain', 'ASC'),)
+
+ 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(id__exact=SITE_ID)
+
+class Package(meta.Model):
+ db_table = 'packages'
+ fields = (
+ meta.CharField('label', 'label', maxlength=20, primary_key=True),
+ meta.CharField('name', 'name', maxlength=30, unique=True),
+ )
+ ordering = (('name', 'ASC'),)
+
+ def __repr__(self):
+ return self.name
+
+class ContentType(meta.Model):
+ db_table = 'content_types'
+ fields = (
+ meta.CharField('name', 'name', maxlength=100),
+ meta.ForeignKey(Package, name='package'),
+ meta.CharField('python_module_name', 'Python module name', maxlength=50),
+ )
+ ordering = (('package', 'ASC'), ('name', 'ASC'),)
+ unique_together = (('package', 'python_module_name'),)
+
+ def __repr__(self):
+ return "%s | %s" % (self.package, 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, 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 Redirect(meta.Model):
+ db_table = 'redirects'
+ fields = (
+ meta.ForeignKey(Site, radio_admin=meta.VERTICAL),
+ meta.CharField('old_path', 'redirect from', maxlength=200, db_index=True,
+ help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'."),
+ meta.CharField('new_path', 'redirect to', maxlength=200, blank=True,
+ help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'."),
+ )
+ unique_together=(('site_id', 'old_path'),)
+ ordering = (('old_path', 'ASC'),)
+ admin = meta.Admin(
+ fields = (
+ (None, {'fields': ('site_id', 'old_path', 'new_path')}),
+ ),
+ list_display = ('__repr__',),
+ list_filter = ('site_id',),
+ search_fields = ('old_path', 'new_path'),
+ )
+
+ def __repr__(self):
+ return "%s ---> %s" % (self.old_path, self.new_path)
+
+class FlatFile(meta.Model):
+ db_table = 'flatfiles'
+ fields = (
+ meta.CharField('url', 'URL', maxlength=100, validator_list=[validators.isAlphaNumericURL],
+ help_text="Example: '/about/contact/'. Make sure to have leading and trailing slashes."),
+ meta.CharField('title', 'title', maxlength=200),
+ meta.TextField('content', 'content', help_text="Full HTML is allowed."),
+ meta.BooleanField('enable_comments', 'enable comments'),
+ meta.CharField('template_name', 'template name', maxlength=70, blank=True,
+ help_text="Example: 'flatfiles/contact_page'. If this isn't provided, the system will use 'flatfiles/default'."),
+ meta.BooleanField('registration_required', 'registration required',
+ help_text="If this is checked, only logged-in users will be able to view the page."),
+ meta.ManyToManyField(Site),
+ )
+ ordering = (('url', 'ASC'),)
+ 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/parts/__init__.py b/django/parts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/parts/__init__.py
diff --git a/django/parts/admin/__init__.py b/django/parts/admin/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/parts/admin/__init__.py
diff --git a/django/parts/admin/doc.py b/django/parts/admin/doc.py
new file mode 100644
index 0000000000..b94f45b198
--- /dev/null
+++ b/django/parts/admin/doc.py
@@ -0,0 +1,93 @@
+"""
+Misc. utility functions/classes for documentation generator
+"""
+
+import re
+from email.Parser import HeaderParser
+from email.Errors import HeaderParseError
+import docutils.core
+import docutils.nodes
+import docutils.parsers.rst.roles
+
+#
+# reST roles
+#
+ROLES = {
+ # role name, base role url (in the admin)
+ 'model' : '/doc/models/%s/',
+ 'view' : '/doc/views/%s/',
+ 'template' : '/doc/templates/%s/',
+ 'filter' : '/doc/filters/#%s',
+ 'tag' : '/doc/tags/#%s',
+}
+
+def trim_docstring(docstring):
+ """
+ Uniformly trims leading/trailing whitespace from docstrings.
+
+ Based on http://www.python.org/peps/pep-0257.html#handling-docstring-indentation
+ """
+ if not docstring or not docstring.strip():
+ return ''
+ # Convert tabs to spaces and split into lines
+ lines = docstring.expandtabs().splitlines()
+ indent = min([len(line) - len(line.lstrip()) for line in lines if line.lstrip()])
+ trimmed = [lines[0].lstrip()] + [line[indent:].rstrip() for line in lines[1:]]
+ return "\n".join(trimmed).strip()
+
+def parse_docstring(docstring):
+ """
+ Parse out the parts of a docstring. Returns (title, body, metadata).
+ """
+ docstring = trim_docstring(docstring)
+ parts = re.split(r'\n{2,}', docstring)
+ title = parts[0]
+ if len(parts) == 1:
+ body = ''
+ metadata = {}
+ else:
+ parser = HeaderParser()
+ try:
+ metadata = parser.parsestr(parts[-1])
+ except HeaderParseError:
+ metadata = {}
+ body = "\n\n".join(parts[1:])
+ else:
+ metadata = dict(metadata.items())
+ if metadata:
+ body = "\n\n".join(parts[1:-1])
+ else:
+ body = "\n\n".join(parts[1:])
+ return title, body, metadata
+
+def parse_rst(text, default_reference_context, thing_being_parsed=None):
+ """
+ Convert the string from reST to an XHTML fragment.
+ """
+ overrides = {
+ 'input_encoding' : 'unicode',
+ 'doctitle_xform' : True,
+ 'inital_header_level' : 3,
+ }
+ if thing_being_parsed:
+ thing_being_parsed = "<%s>" % thing_being_parsed
+ parts = docutils.core.publish_parts(text, source_path=thing_being_parsed,
+ destination_path=None, writer_name='html',
+ settings_overrides={'default_reference_context' : default_reference_context})
+ return parts['fragment']
+
+def create_reference_role(rolename, urlbase):
+ def _role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+ node = docutils.nodes.reference(rawtext, text, refuri=(urlbase % text), **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] % text), **options)
+ return [node], []
+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():
+ create_reference_role(name, urlbase)
diff --git a/django/parts/auth/__init__.py b/django/parts/auth/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/parts/auth/__init__.py
diff --git a/django/parts/auth/anonymoususers.py b/django/parts/auth/anonymoususers.py
new file mode 100644
index 0000000000..2a86911f15
--- /dev/null
+++ b/django/parts/auth/anonymoususers.py
@@ -0,0 +1,48 @@
+"""
+Anonymous users
+"""
+
+class AnonymousUser:
+
+ 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_groups(self):
+ return []
+
+ def set_groups(self, group_id_list):
+ raise NotImplementedError
+
+ def get_permissions(self):
+ return []
+
+ def set_permissions(self, permission_id_list):
+ raise NotImplementedError
+
+ def has_perm(self, perm):
+ return False
+
+ def get_and_delete_messages(self):
+ return []
+
+ def add_session(self, session_md5, start_time):
+ "Creates Session for this User, saves it, and returns the new object"
+ raise NotImplementedError
+
+ def is_anonymous(self):
+ return True
diff --git a/django/parts/auth/formfields.py b/django/parts/auth/formfields.py
new file mode 100644
index 0000000000..3565331c8f
--- /dev/null
+++ b/django/parts/auth/formfields.py
@@ -0,0 +1,46 @@
+from django.models.auth import sessions, 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.COOKIES.has_key(sessions.TEST_COOKIE_NAME) or self.request.COOKIES[sessions.TEST_COOKIE_NAME] != sessions.TEST_COOKIE_VALUE):
+ 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/__init__.py b/django/parts/media/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/parts/media/__init__.py
diff --git a/django/parts/media/photos.py b/django/parts/media/photos.py
new file mode 100644
index 0000000000..a14b3de19b
--- /dev/null
+++ b/django/parts/media/photos.py
@@ -0,0 +1,6 @@
+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/templatetags/__init__.py b/django/templatetags/__init__.py
new file mode 100644
index 0000000000..538da5b354
--- /dev/null
+++ b/django/templatetags/__init__.py
@@ -0,0 +1,7 @@
+from django.conf.settings import INSTALLED_APPS
+
+for a in INSTALLED_APPS:
+ try:
+ __path__.extend(__import__(a + '.templatetags', '', '', ['']).__path__)
+ except ImportError:
+ pass
diff --git a/django/templatetags/comments.py b/django/templatetags/comments.py
new file mode 100644
index 0000000000..adfd3cedac
--- /dev/null
+++ b/django/templatetags/comments.py
@@ -0,0 +1,331 @@
+"Custom template tags for user comments"
+
+from django.core import template
+from django.core.exceptions import ObjectDoesNotExist
+from django.models.comments import comments, freecomments
+from django.models.core import contenttypes
+import re
+
+COMMENT_FORM = '''
+{% if display_form %}
+<form {% if photos_optional or photos_required %}enctype="multipart/form-data" {% endif %}action="/comments/post/" method="post">
+
+{% if user.is_anonymous %}
+<p>Username: <input type="text" name="username" id="id_username" /><br />Password: <input type="password" name="password" id="id_password" /> (<a href="/accounts/password_reset/">Forgotten your password?</a>)</p>
+{% else %}
+<p>Username: <strong>{{ user.username }}</strong> (<a href="/accounts/logout/">Log out</a>)</p>
+{% endif %}
+
+{% if ratings_optional or ratings_required %}
+<p>Ratings ({% if ratings_required %}Required{% else %}Optional{% endif %}):</p>
+<table>
+<tr><th>&nbsp;</th>{% for value in rating_range %}<th>{{ value }}</th>{% endfor %}</tr>
+{% for rating in rating_choices %}
+<tr><th>{{ rating }}</th>{% for value in rating_range %}<th><input type="radio" name="rating{{ forloop.parentloop.counter }}" value="{{ value }}" /></th>{% endfor %}</tr>
+{% endfor %}
+</table>
+<input type="hidden" name="rating_options" value="{{ rating_options }}" />
+{% endif %}
+
+{% if photos_optional or photos_required %}
+<p>Post a photo ({% if photos_required %}Required{% else %}Optional{% endif %}): <input type="file" name="photo" /></p>
+<input type="hidden" name="photo_options" value="{{ photo_options }}" />
+{% endif %}
+
+<p>Comment:<br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
+
+<input type="hidden" name="options" value="{{ options }}" />
+<input type="hidden" name="target" value="{{ target }}" />
+<input type="hidden" name="gonzo" value="{{ hash }}" />
+<p><input type="submit" name="preview" value="Preview comment" /></p>
+</form>
+{% endif %}
+'''
+
+FREE_COMMENT_FORM = '''
+{% if display_form %}
+<form enctype="multipart/form-data" action="/comments/postfree/" method="post">
+<p>Your name: <input type="text" id="id_person_name" name="person_name" /></p>
+<p>Comment:<br /><textarea name="comment" id="id_comment" rows="10" cols="60"></textarea></p>
+<input type="hidden" name="options" value="{{ options }}" />
+<input type="hidden" name="target" value="{{ target }}" />
+<input type="hidden" name="gonzo" value="{{ hash }}" />
+<p><input type="submit" name="preview" value="Preview comment" /></p>
+</form>
+{% endif %}
+'''
+
+class CommentFormNode(template.Node):
+ def __init__(self, content_type, obj_id_lookup_var, obj_id, free,
+ photos_optional=False, photos_required=False, photo_options='',
+ ratings_optional=False, ratings_required=False, rating_options='',
+ is_public=True):
+ self.content_type = content_type
+ self.obj_id_lookup_var, self.obj_id, self.free = obj_id_lookup_var, obj_id, free
+ self.photos_optional, self.photos_required = photos_optional, photos_required
+ self.ratings_optional, self.ratings_required = ratings_optional, ratings_required
+ self.photo_options, self.rating_options = photo_options, rating_options
+ self.is_public = is_public
+
+ def render(self, context):
+ from django.utils.text import normalize_newlines
+ import base64
+ context.push()
+ if self.obj_id_lookup_var is not None:
+ try:
+ self.obj_id = template.resolve_variable(self.obj_id_lookup_var, context)
+ except template.VariableDoesNotExist:
+ return ''
+ # Validate that this object ID is valid for this content-type.
+ # We only have to do this validation if obj_id_lookup_var is provided,
+ # because do_comment_form() validates hard-coded object IDs.
+ try:
+ self.content_type.get_object_for_this_type(id__exact=self.obj_id)
+ except ObjectDoesNotExist:
+ context['display_form'] = False
+ else:
+ 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)):
+ 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'])
+ default_form = 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'])
+ default_form = COMMENT_FORM
+ output = template.Template(default_form).render(context)
+ context.pop()
+ return output
+
+class CommentCountNode(template.Node):
+ def __init__(self, package, module, context_var_name, obj_id, var_name, free):
+ self.package, self.module = package, module
+ self.context_var_name, self.obj_id = context_var_name, obj_id
+ 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
+ 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)
+ context[self.var_name] = comment_count
+ return ''
+
+class CommentListNode(template.Node):
+ def __init__(self, package, module, context_var_name, obj_id, var_name, free):
+ self.package, self.module = package, module
+ self.context_var_name, self.obj_id = context_var_name, obj_id
+ self.var_name, self.free = var_name, free
+
+ 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
+ if self.context_var_name is not None:
+ try:
+ self.obj_id = template.resolve_variable(self.context_var_name, context)
+ except template.VariableDoesNotExist:
+ 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': (('submit_date', 'ASC'),),
+ }
+ 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:
+ 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'])
+ 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:
+ 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
+ return ''
+
+class DoCommentForm:
+ """
+ Displays a comment form for the given params. Syntax:
+ {% comment_form for [pkg].[py_module_name] [context_var_containing_obj_id] with [list of options] %}
+ Example usage:
+ {% comment_form for lcom.eventtimes event.id with is_public yes photos_optional thumbs,200,400 ratings_optional scale:1-5|first_option|second_option %}
+ [context_var_containing_obj_id] can be a hard-coded integer or a variable containing the ID.
+ """
+ def __init__(self, free, tag_name):
+ self.free, self.tag_name = free, tag_name
+
+ def __call__(self, parser, token):
+ tokens = token.contents.split()
+ if len(tokens) < 4:
+ raise template.TemplateSyntaxError, "'%s' tag requires at least 3 arguments" % self.tag_name
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError, "Second argument in '%s' tag must be 'for'" % self.tag_name
+ try:
+ package, module = tokens[2].split('.')
+ except ValueError: # unpack list of wrong size
+ raise template.TemplateSyntaxError, "Third argument in '%s' tag must be in the format 'package.module'" % self.tag_name
+ try:
+ content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module)
+ except contenttypes.ContentTypeDoesNotExist:
+ raise template.TemplateSyntaxError, "'%s' tag has invalid content-type '%s.%s'" % (self.tag_name, package, module)
+ obj_id_lookup_var, obj_id = None, None
+ if tokens[3].isdigit():
+ obj_id = tokens[3]
+ try: # ensure the object ID is valid
+ content_type.get_object_for_this_type(id__exact=obj_id)
+ except ObjectDoesNotExist:
+ raise template.TemplateSyntaxError, "'%s' tag refers to %s object with ID %s, which doesn't exist" % (self.tag_name, content_type.name, obj_id)
+ else:
+ obj_id_lookup_var = tokens[3]
+ kwargs = {}
+ if len(tokens) > 4:
+ if tokens[4] != 'with':
+ raise template.TemplateSyntaxError, "Fourth argument in '%s' tag must be 'with'" % self.tag_name
+ for option, args in zip(tokens[5::2], tokens[6::2]):
+ if option in ('photos_optional', 'photos_required') and not self.free:
+ # VALIDATION ##############################################
+ option_list = args.split(',')
+ if len(option_list) % 3 != 0:
+ raise template.TemplateSyntaxError, "Incorrect number of comma-separated arguments to '%s' tag" % self.tag_name
+ for opt in option_list[::3]:
+ if not opt.isalnum():
+ raise template.TemplateSyntaxError, "Invalid photo directory name in '%s' tag: '%s'" % (self.tag_name, 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 '%s' tag: '%s'. Only values between %s and %s are allowed." % (self.tag_name, opt, comments.MIN_PHOTO_DIMENSION, comments.MAX_PHOTO_DIMENSION)
+ # VALIDATION ENDS #########################################
+ kwargs[option] = True
+ kwargs['photo_options'] = args
+ elif option in ('ratings_optional', 'ratings_required') and not self.free:
+ # VALIDATION ##############################################
+ if 2 < len(args.split('|')) > 9:
+ raise template.TemplateSyntaxError, "Incorrect number of '%s' options in '%s' tag. Use between 2 and 8." % (option, self.tag_name)
+ if re.match('^scale:\d+\-\d+\:$', args.split('|')[0]):
+ raise template.TemplateSyntaxError, "Invalid 'scale' in '%s' tag's '%s' options" % (self.tag_name, option)
+ # VALIDATION ENDS #########################################
+ kwargs[option] = True
+ kwargs['rating_options'] = args
+ elif option in ('is_public'):
+ kwargs[option] = (args == 'true')
+ else:
+ raise template.TemplateSyntaxError, "'%s' tag got invalid parameter '%s'" % (self.tag_name, option)
+ return CommentFormNode(content_type, obj_id_lookup_var, obj_id, self.free, **kwargs)
+
+class DoCommentCount:
+ """
+ Gets comment count for the given params and populates the template context
+ with a variable containing that value, whose name is defined by the 'as'
+ clause. Syntax:
+ {% get_comment_count for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] %}
+ Example usage:
+ {% get_comment_count for lcom.eventtimes event.id as comment_count %}
+ Note: [context_var_containing_obj_id] can also be a hard-coded integer, like this:
+ {% get_comment_count for lcom.eventtimes 23 as comment_count %}
+ """
+ def __init__(self, free, tag_name):
+ self.free, self.tag_name = free, tag_name
+
+ def __call__(self, parser, token):
+ tokens = token.contents.split()
+ # Now tokens is a list like this:
+ # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list']
+ if len(tokens) != 6:
+ raise template.TemplateSyntaxError, "%s block tag requires 5 arguments" % self.tag_name
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError, "Second argument in '%s' tag must be 'for'" % self.tag_name
+ try:
+ package, module = tokens[2].split('.')
+ except ValueError: # unpack list of wrong size
+ raise template.TemplateSyntaxError, "Third argument in '%s' tag must be in the format 'package.module'" % self.tag_name
+ try:
+ content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module)
+ except contenttypes.ContentTypeDoesNotExist:
+ raise template.TemplateSyntaxError, "'%s' tag has invalid content-type '%s.%s'" % (self.tag_name, package, module)
+ var_name, obj_id = None, None
+ if tokens[3].isdigit():
+ obj_id = tokens[3]
+ try: # ensure the object ID is valid
+ content_type.get_object_for_this_type(id__exact=obj_id)
+ except ObjectDoesNotExist:
+ raise template.TemplateSyntaxError, "'%s' tag refers to %s object with ID %s, which doesn't exist" % (self.tag_name, content_type.name, obj_id)
+ else:
+ var_name = tokens[3]
+ if tokens[4] != 'as':
+ raise template.TemplateSyntaxError, "Fourth argument in '%s' must be 'as'" % self.tag_name
+ return CommentCountNode(package, module, var_name, obj_id, tokens[5], self.free)
+
+class DoGetCommentList:
+ """
+ Gets comments for the given params and populates the template context with
+ a special comment_package variable, whose name is defined by the 'as'
+ clause. Syntax:
+ {% get_comment_list for [pkg].[py_module_name] [context_var_containing_obj_id] as [varname] %}
+ Example usage:
+ {% get_comment_list for lcom.eventtimes event.id as comment_list %}
+ Note: [context_var_containing_obj_id] can also be a hard-coded integer, like this:
+ {% get_comment_list for lcom.eventtimes 23 as comment_list %}
+ """
+ def __init__(self, free, tag_name):
+ self.free, self.tag_name = free, tag_name
+
+ def __call__(self, parser, token):
+ tokens = token.contents.split()
+ # Now tokens is a list like this:
+ # ['get_comment_list', 'for', 'lcom.eventtimes', 'event.id', 'as', 'comment_list']
+ if len(tokens) != 6:
+ raise template.TemplateSyntaxError, "%s block tag requires 5 arguments" % self.tag_name
+ if tokens[1] != 'for':
+ raise template.TemplateSyntaxError, "Second argument in '%s' tag must be 'for'" % self.tag_name
+ try:
+ package, module = tokens[2].split('.')
+ except ValueError: # unpack list of wrong size
+ raise template.TemplateSyntaxError, "Third argument in '%s' tag must be in the format 'package.module'" % self.tag_name
+ try:
+ content_type = contenttypes.get_object(package__label__exact=package, python_module_name__exact=module)
+ except contenttypes.ContentTypeDoesNotExist:
+ raise template.TemplateSyntaxError, "'%s' tag has invalid content-type '%s.%s'" % (self.tag_name, package, module)
+ var_name, obj_id = None, None
+ if tokens[3].isdigit():
+ obj_id = tokens[3]
+ try: # ensure the object ID is valid
+ content_type.get_object_for_this_type(id__exact=obj_id)
+ except ObjectDoesNotExist:
+ raise template.TemplateSyntaxError, "'%s' tag refers to %s object with ID %s, which doesn't exist" % (self.tag_name, content_type.name, obj_id)
+ else:
+ var_name = tokens[3]
+ if tokens[4] != 'as':
+ raise template.TemplateSyntaxError, "Fourth argument in '%s' must be 'as'" % self.tag_name
+ return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free)
+
+# registration comments
+template.register_tag('get_comment_list', DoGetCommentList(free=False, tag_name='get_comment_list'))
+template.register_tag('comment_form', DoCommentForm(free=False, tag_name='comment_form'))
+template.register_tag('get_comment_count', DoCommentCount(free=False, tag_name='get_comment_count'))
+# free comments
+template.register_tag('get_free_comment_list', DoGetCommentList(free=True, tag_name='get_free_comment_list'))
+template.register_tag('free_comment_form', DoCommentForm(free=True, tag_name='free_comment_form'))
+template.register_tag('get_free_comment_count', DoCommentCount(free=True, tag_name='get_free_comment_count'))
diff --git a/django/templatetags/log.py b/django/templatetags/log.py
new file mode 100644
index 0000000000..3f858fa1f8
--- /dev/null
+++ b/django/templatetags/log.py
@@ -0,0 +1,45 @@
+from django.models.auth import log
+from django.core import template
+
+class AdminLogNode(template.Node):
+ def __init__(self, limit, varname, user):
+ self.limit, self.varname, self.user = limit, varname, user
+
+ def __repr__(self):
+ return "<GetAdminLog 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)
+ return ''
+
+class DoGetAdminLog:
+ """
+ Populates a template variable with the admin log for the given criteria.
+ Usage:
+ {% get_admin_log [limit] as [varname] for_user [context_var_containing_user_obj] %}
+ Examples:
+ {% get_admin_log 10 as admin_log for_user 23 %}
+ {% get_admin_log 10 as admin_log for_user user %}
+ {% get_admin_log 10 as admin_log %}
+ Note that [context_var_containing_user_obj] can be a hard-coded integer (user ID) or the
+ name of a template context variable containing the user object whose ID you want.
+ """
+ def __init__(self, tag_name):
+ self.tag_name = tag_name
+
+ def __call__(self, parser, token):
+ tokens = token.contents.split()
+ if len(tokens) < 4:
+ raise template.TemplateSyntaxError, "'%s' statements require two arguments" % self.tag_name
+ if not tokens[1].isdigit():
+ raise template.TemplateSyntaxError, "First argument in '%s' must be an integer" % self.tag_name
+ if tokens[2] != 'as':
+ raise template.TemplateSyntaxError, "Second argument in '%s' must be 'as'" % self.tag_name
+ if len(tokens) > 4:
+ if tokens[4] != 'for_user':
+ raise template.TemplateSyntaxError, "Fourth argument in '%s' must be 'for_user'" % self.tag_name
+ return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(len(tokens) > 5 and tokens[5] or None))
+
+template.register_tag('get_admin_log', DoGetAdminLog('get_admin_log'))
diff --git a/django/tests/__init__.py b/django/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/tests/__init__.py
diff --git a/django/tests/cache_tests.py b/django/tests/cache_tests.py
new file mode 100644
index 0000000000..a9cce041fa
--- /dev/null
+++ b/django/tests/cache_tests.py
@@ -0,0 +1,119 @@
+"""
+Unit tests for django.core.cache
+
+If you don't have memcached running on localhost port 11211, the memcached tests
+will fail.
+"""
+
+from django.core import cache
+import unittest
+import time
+
+# functions/classes for complex data type tests
+def f():
+ return 42
+class C:
+ def m(n):
+ return 24
+
+class CacheBackendsTest(unittest.TestCase):
+
+ def testBackends(self):
+ sc = cache.get_cache('simple://')
+ mc = cache.get_cache('memcached://127.0.0.1:11211/')
+ self.failUnless(isinstance(sc, cache._SimpleCache))
+ self.failUnless(isinstance(mc, cache._MemcachedCache))
+
+ def testInvalidBackends(self):
+ self.assertRaises(cache.InvalidCacheBackendError, cache.get_cache, 'nothing://foo/')
+ self.assertRaises(cache.InvalidCacheBackendError, cache.get_cache, 'not a uri')
+
+ def testDefaultTimeouts(self):
+ sc = cache.get_cache('simple:///?timeout=15')
+ mc = cache.get_cache('memcached://127.0.0.1:11211/?timeout=15')
+ self.assertEquals(sc.default_timeout, 15)
+ self.assertEquals(sc.default_timeout, 15)
+
+class SimpleCacheTest(unittest.TestCase):
+
+ def setUp(self):
+ self.cache = cache.get_cache('simple://')
+
+ def testGetSet(self):
+ self.cache.set('key', 'value')
+ self.assertEqual(self.cache.get('key'), 'value')
+
+ def testNonExistantKeys(self):
+ self.assertEqual(self.cache.get('does not exist'), None)
+ self.assertEqual(self.cache.get('does not exist', 'bang!'), 'bang!')
+
+ def testGetMany(self):
+ self.cache.set('a', 'a')
+ self.cache.set('b', 'b')
+ self.cache.set('c', 'c')
+ self.cache.set('d', 'd')
+ self.assertEqual(self.cache.get_many(['a', 'c', 'd']), {'a' : 'a', 'c' : 'c', 'd' : 'd'})
+ self.assertEqual(self.cache.get_many(['a', 'b', 'e']), {'a' : 'a', 'b' : 'b'})
+
+ def testDelete(self):
+ self.cache.set('key1', 'spam')
+ self.cache.set('key2', 'eggs')
+ self.assertEqual(self.cache.get('key1'), 'spam')
+ self.cache.delete('key1')
+ self.assertEqual(self.cache.get('key1'), None)
+ self.assertEqual(self.cache.get('key2'), 'eggs')
+
+ def testHasKey(self):
+ self.cache.set('hello', 'goodbye')
+ self.assertEqual(self.cache.has_key('hello'), True)
+ self.assertEqual(self.cache.has_key('goodbye'), False)
+
+ def testDataTypes(self):
+ items = {
+ 'string' : 'this is a string',
+ 'int' : 42,
+ 'list' : [1, 2, 3, 4],
+ 'tuple' : (1, 2, 3, 4),
+ 'dict' : {'A': 1, 'B' : 2},
+ 'function' : f,
+ 'class' : C,
+ }
+ for (key, value) in items.items():
+ self.cache.set(key, value)
+ self.assertEqual(self.cache.get(key), value)
+
+ def testExpiration(self):
+ self.cache.set('expire', 'very quickly', 1)
+ time.sleep(2)
+ self.assertEqual(self.cache.get('expire'), None)
+
+ def testCull(self):
+ c = cache.get_cache('simple://?max_entries=9&cull_frequency=3')
+ for i in range(10):
+ c.set('culltest%i' % i, i)
+ n = 0
+ for i in range(10):
+ if c.get('culltest%i' % i):
+ n += 1
+ self.assertEqual(n, 6)
+
+ def testCullAll(self):
+ c = cache.get_cache('simple://?max_entries=9&cull_frequency=0')
+ for i in range(10):
+ c.set('cullalltest%i' % i, i)
+ for i in range(10):
+ self.assertEqual(self.cache.get('cullalltest%i' % i), None)
+
+class MemcachedCacheTest(SimpleCacheTest):
+
+ def setUp(self):
+ self.cache = cache.get_cache('memcached://127.0.0.1:11211/')
+
+ testCull = testCullAll = lambda s: None
+
+def tests():
+ s = unittest.TestLoader().loadTestsFromName(__name__)
+ unittest.TextTestRunner(verbosity=0).run(s)
+
+if __name__ == "__main__":
+ tests()
diff --git a/django/tests/template_inheritance.py b/django/tests/template_inheritance.py
new file mode 100644
index 0000000000..35de1457e3
--- /dev/null
+++ b/django/tests/template_inheritance.py
@@ -0,0 +1,102 @@
+from django.core import template, template_loader
+
+# SYNTAX --
+# 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
+TEMPLATE_TESTS = {
+ # Standard template with no inheritance
+ 'test01': ("1{% block first %}_{% endblock %}3{% block second %}_{% endblock %}", {}, '1_3_'),
+
+ # Standard two-level inheritance
+ 'test02': ("{% extends 'test01' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'),
+
+ # Three-level with no redefinitions on third level
+ 'test03': ("{% extends 'test02' %}", {}, '1234'),
+
+ # Two-level with no redefinitions on second level
+ 'test04': ("{% extends 'test01' %}", {}, '1_3_'),
+
+ # Two-level with double quotes instead of single quotes
+ 'test05': ('{% extends "test02" %}', {}, '1234'),
+
+ # Three-level with variable parent-template name
+ 'test06': ("{% extends foo %}", {'foo': 'test02'}, '1234'),
+
+ # Two-level with one block defined, one block not defined
+ 'test07': ("{% extends 'test01' %}{% block second %}5{% endblock %}", {}, '1_35'),
+
+ # Three-level with one block defined on this level, two blocks defined next level
+ 'test08': ("{% extends 'test02' %}{% block second %}5{% endblock %}", {}, '1235'),
+
+ # Three-level with second and third levels blank
+ 'test09': ("{% extends 'test04' %}", {}, '1_3_'),
+
+ # Three-level with space NOT in a block -- should be ignored
+ 'test10': ("{% extends 'test04' %} ", {}, '1_3_'),
+
+ # Three-level with both blocks defined on this level, but none on second level
+ 'test11': ("{% extends 'test04' %}{% block first %}2{% endblock %}{% block second %}4{% endblock %}", {}, '1234'),
+
+ # Three-level with this level providing one and second level providing the other
+ 'test12': ("{% extends 'test07' %}{% block first %}2{% endblock %}", {}, '1235'),
+
+ # Three-level with this level overriding second level
+ 'test13': ("{% extends 'test02' %}{% block first %}a{% endblock %}{% block second %}b{% endblock %}", {}, '1a3b'),
+
+ # A block defined only in a child template shouldn't be displayed
+ 'test14': ("{% extends 'test01' %}{% block newblock %}NO DISPLAY{% endblock %}", {}, '1_3_'),
+
+ # A block within another block
+ 'test15': ("{% extends 'test01' %}{% block first %}2{% block inner %}inner{% endblock %}{% endblock %}", {}, '12inner3_'),
+
+ # A block within another block (level 2)
+ 'test16': ("{% extends 'test15' %}{% block inner %}out{% endblock %}", {}, '12out3_'),
+
+ # {% load %} tag (parent -- setup for test-exception04)
+ 'test17': ("{% load polls.polls %}{% block first %}1234{% endblock %}", {}, '1234'),
+
+ # {% load %} tag (standard usage, without inheritance)
+ 'test18': ("{% load polls.polls %}{% voteratio choice poll 400 %}5678", {}, '05678'),
+
+ # {% load %} tag (within a child template)
+ 'test19': ("{% extends 'test01' %}{% block first %}{% load polls.polls %}{% voteratio choice poll 400 %}5678{% endblock %}", {}, '1056783_'),
+
+ # Raise exception for invalid template name
+ 'test-exception01': ("{% extends 'nonexistent' %}", {}, template.TemplateSyntaxError),
+
+ # Raise exception for invalid template name (in variable)
+ 'test-exception02': ("{% extends nonexistent %}", {}, template.TemplateSyntaxError),
+
+ # Raise exception for extra {% extends %} tags
+ 'test-exception03': ("{% extends 'test01' %}{% block first %}2{% endblock %}{% extends 'test16' %}", {}, template.TemplateSyntaxError),
+
+ # Raise exception for custom tags used in child with {% load %} tag in parent, not in child
+ 'test-exception04': ("{% extends 'test17' %}{% block first %}{% votegraph choice poll 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError),
+}
+
+# This replaces the standard template_loader.
+def test_template_loader(template_name):
+ try:
+ return TEMPLATE_TESTS[template_name][0]
+ except KeyError:
+ raise template.TemplateDoesNotExist, template_name
+template_loader.load_template_source = test_template_loader
+
+def run_tests():
+ tests = TEMPLATE_TESTS.items()
+ tests.sort()
+ for name, vals in tests:
+ try:
+ output = template_loader.get_template(name).render(template.Context(vals[1]))
+ except Exception, e:
+ if e.__class__ == vals[2]:
+ print "%s -- Passed" % name
+ else:
+ print "%s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e)
+ continue
+ if output == vals[2]:
+ print "%s -- Passed" % name
+ else:
+ print "%s -- FAILED. Expected %r, got %r" % (name, vals[2], output)
+
+if __name__ == "__main__":
+ run_tests()
diff --git a/django/tests/template_tests.py b/django/tests/template_tests.py
new file mode 100644
index 0000000000..3b082818bd
--- /dev/null
+++ b/django/tests/template_tests.py
@@ -0,0 +1,707 @@
+"""
+Unit tests for template.py
+
+These tests assume the following template syntax:
+
+FILTER_SEPARATOR = '|'
+VARIABLE_ATTRIBUTE_SEPARATOR = '.'
+BLOCK_TAG_START = '{%'
+BLOCK_TAG_END = '%}'
+VARIABLE_TAG_START = '{{'
+VARIABLE_TAG_END = '}}'
+"""
+
+from django.core import template
+import unittest
+
+class RandomSyntaxErrorsCheck(unittest.TestCase):
+
+ def testTagsOnOneLine(self):
+ "Tags straddling more than one line are not interpreted"
+ c = template.Context({'key':'value'})
+ t = template.Template('<h1>{{key\n}}</h1>')
+ expected = '<h1>{{key\n}}</h1>'
+ self.assertEqual(expected, t.render(c))
+
+class PlainTextCheck(unittest.TestCase):
+
+ def testPlainText(self):
+ "Plain text should go through the template parser untouched"
+ c = template.Context()
+ t = template.Template('<h1>Success</h1>')
+ expected = '<h1>Success</h1>'
+ self.assertEqual(expected, t.render(c))
+
+class VariableSubstitutionCheck(unittest.TestCase):
+
+ def testSingleTag(self):
+ "Variables should be replaced with their value in the current context"
+ c = template.Context({'headline':'Success'})
+ t = template.Template('<h1>{{headline}}</h1>')
+ expected = '<h1>Success</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testDoubleTag(self):
+ "More than one replacement variable is allowed in a template"
+ c = template.Context({'firsttag':'it', 'secondtag':'worked'})
+ t = template.Template('<h1>{{firsttag}} {{secondtag}}</h1>')
+ expected = '<h1>it worked</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNonexistentVariable(self):
+ "Fail silently when a variable is not found in the current context"
+ c = template.Context({})
+ t = template.Template('<h1>{{unknownvar}}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariablesWithSpaces(self):
+ "A replacement-variable tag may not contain more than one word"
+ t = '<h1>{{multi word tag}}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testEmptyTag(self):
+ "Raise TemplateSyntaxError for empty variable tags"
+ t = '{{ }}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+ t = '{{ }}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testIntegerContextValue(self):
+ "Accept integers as variable values"
+ c = template.Context({'var':55})
+ t = template.Template('<h1>{{var}}</h1>')
+ expected = '<h1>55</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def textIntegerContextKey(self):
+ "Accept integers as variable keys"
+ c = template.Context({55:'var'})
+ t = template.Template('<h1>{{55}}</h1>')
+ expected = '<h1>var</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableAttributeAccess1(self):
+ "Attribute syntax allows a template to call an object's attribute"
+ class AClass: pass
+ obj = AClass()
+ obj.att = 'attvalue'
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.att }}</h1>')
+ expected = '<h1>attvalue</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableAttributeAccess2(self):
+ "Attribute syntax allows a template to call an object's attribute (with getattr defined)"
+ class AClass:
+ def __getattr__(self, attr):
+ return "attvalue"
+ obj = AClass()
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.att }}</h1>')
+ expected = '<h1>attvalue</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableAttributeAccessMultiple(self):
+ "Multiple levels of attribute access are allowed"
+ class AClass: pass
+ obj = AClass()
+ obj.article = AClass()
+ obj.article.section = AClass()
+ obj.article.section.title = 'Headline'
+ c = template.Context({'obj':obj})
+ t = template.Template('<h1>{{ obj.article.section.title }}</h1>')
+ expected = '<h1>Headline</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNonexistentVariableAttributeObject(self):
+ "Fail silently when a variable's attribute isn't found"
+ class AClass: pass
+ obj = AClass()
+ obj.att = 'attvalue'
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.nonexistentatt }}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testIllegalUnderscoreInVariableName(self):
+ "Raise TemplateSyntaxError when trying to access a variable beginning with an underscore"
+ t = '<h1>{{ var._att }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+ t = '<h1>{{ _att }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testIllegalCharacterInVariableName(self):
+ "Raise TemplateSyntaxError when trying to access a variable containing an illegal character"
+ t = '<h1>{{ (blah }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+ t = '<h1>{{ (blah.test) }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+ t = '<h1>{{ bl(ah.test) }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testVariableAttributeDictionary(self):
+ "Attribute syntax allows a template to call a dictionary key's value"
+ obj = {'att':'attvalue'}
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.att }}</h1>')
+ expected = '<h1>attvalue</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNonexistentVariableAttributeDictionary(self):
+ "Fail silently when a variable's dictionary key isn't found"
+ obj = {'att':'attvalue'}
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.nonexistentatt }}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableAttributeCallable(self):
+ "Attribute syntax allows a template to call a simple method"
+ class AClass:
+ def hello(self): return 'hi'
+ obj = AClass()
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.hello }}</h1>')
+ expected = '<h1>hi</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableAttributeCallableWrongArguments(self):
+ "Fail silently when accessing a non-simple method"
+ class AClass:
+ def hello(self, name): return 'hi, %s' % name
+ obj = AClass()
+ c = template.Context({'var':obj})
+ t = template.Template('<h1>{{ var.hello }}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+class VariableFiltersCheck(unittest.TestCase):
+
+ def setUp(self):
+ self.c = template.Context({'var':'Hello There Programmer'})
+
+ def tearDown(self):
+ self.c = None
+
+ def testUpper(self):
+ "The 'upper' filter converts a string into all uppercase"
+ t = template.Template('<h1>{{ var|upper }}</h1>')
+ expected = '<h1>HELLO THERE PROGRAMMER</h1>'
+ self.assertEqual(expected, t.render(self.c))
+
+ def testLower(self):
+ "The 'lower' filter converts a string into all lowercase"
+ t = template.Template('<h1>{{ var|lower }}</h1>')
+ expected = '<h1>hello there programmer</h1>'
+ self.assertEqual(expected, t.render(self.c))
+
+ def testUpperThenLower(self):
+ "Filters may be applied in succession (upper|lower)"
+ t = template.Template('<h1>{{ var|upper|lower }}</h1>')
+ expected = '<h1>hello there programmer</h1>'
+ self.assertEqual(expected, t.render(self.c))
+
+ def testLowerThenUpper(self):
+ "Filters may be applied in succession (lower|upper)"
+ t = template.Template('<h1>{{ var|lower|upper }}</h1>')
+ expected = '<h1>HELLO THERE PROGRAMMER</h1>'
+ self.assertEqual(expected, t.render(self.c))
+
+ def testSpaceBetweenVariableAndFilterPipe(self):
+ "Raise TemplateSyntaxError for space between a variable and filter pipe"
+ t = '<h1>{{ var |lower }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testSpaceBetweenFilterPipeAndFilterName1(self):
+ "Raise TemplateSyntaxError for space after a filter pipe"
+ t = '<h1>{{ var| lower }}</h1>'
+ expected = '<h1>Hello There Programmer</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testNonexistentFilter(self):
+ "Raise TemplateSyntaxError for a nonexistent filter"
+ t = '<h1>{{ var|nonexistentfilter }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testDefaultFilter1(self):
+ "Ignore the default argument when a variable passed through the 'default' filter already exists"
+ c = template.Context({'var':'Variable'})
+ t = template.Template('<h1>{{ var|default:"Default" }}</h1>')
+ expected = '<h1>Variable</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testDefaultFilter2(self):
+ "Use the default argument when a variable passed through the 'default' filter doesn't exist"
+ c = template.Context({'var':'Variable'})
+ t = template.Template('<h1>{{ nonvar|default:"Default" }}</h1>')
+ expected = '<h1>Default</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testDefaultFilter3(self):
+ "Use the default argument when a variable passed through the 'default' filter doesn't exist (spaces)"
+ c = template.Context({'var':'Variable'})
+ t = template.Template('<h1>{{ nonvar|default:"Default value" }}</h1>')
+ expected = '<h1>Default value</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testDefaultFilter4(self):
+ "Use the default argument when a variable passed through the 'default' filter doesn't exist (quoted)"
+ c = template.Context({'var':'Variable'})
+ t = template.Template('<h1>{{ nonvar|default:"Default \"quoted\" value" }}</h1>')
+ expected = '<h1>Default "quoted" value</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testDefaultFilter4(self):
+ "Use the default argument when a variable passed through the 'default' filter doesn't exist (escaped backslash)"
+ c = template.Context({'var':'Variable'})
+ t = template.Template('<h1>{{ nonvar|default:"Default \\\\ slash" }}</h1>')
+ expected = '<h1>Default \\ slash</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testDefaultFilter4(self):
+ "Use the default argument when a variable passed through the 'default' filter doesn't exist (single backslash)"
+ t = '<h1>{{ nonvar|default:"Default \\ slash" }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testIllegalCharacterInFilterName(self):
+ "Raise TemplateSyntaxError when trying to access a filter containing an illegal character"
+ t = '<h1>{{ blah|(lower) }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+ t = '<h1>{{ blah|low(er) }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+class BlockTagCheck(unittest.TestCase):
+
+ def testNonexistentTag(self):
+ "Raise TemplateSyntaxError for invalid block tags"
+ t = '<h1>{% not-a-tag %}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testEmptyTag(self):
+ "Raise TemplateSyntaxError for empty block tags"
+ t = '{% %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+class FirstOfCheck(unittest.TestCase):
+
+ def testFirstOfDisplaysFirstIfSet(self):
+ "A firstof tag should display the first item if it evaluates to true somehow"
+ c = template.Context({'first': 'one', 'second': 'two'})
+ t = template.Template('<h1>{% firstof first second %}</h1>')
+ expected = '<h1>one</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testFirstOfDisplaysSecondIfFirstIsFalse(self):
+ "A firstof tag should display the second item if it evaluates to true and the first is false"
+ c = template.Context({'first': '', 'second': 'two'})
+ t = template.Template('<h1>{% firstof first second %}</h1>')
+ expected = '<h1>two</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testFirstOfRaisesErrorIfEmpty(self):
+ "A firstof tag should raise a syntax error if it doesn't have any arguments"
+ t = '{% firstof %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testFirstOfDoesNothingIfAllAreFalse(self):
+ "A firstof tag should display nothing if no arguments evaluate to true"
+ c = template.Context({'first': '', 'second': False})
+ t = template.Template('<h1>{% firstof first second third %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testFirstOfWorksWithInts(self):
+ "Can a firstof tag display an integer?"
+ c = template.Context({'first': 1, 'second': False})
+ t = template.Template('<h1>{% firstof first second %}</h1>')
+ expected = '<h1>1</h1>'
+ self.assertEqual(expected, t.render(c))
+
+class IfStatementCheck(unittest.TestCase):
+
+ def testSingleIfStatementTrue(self):
+ "An if statement should display its contents if the test evaluates true"
+ c = template.Context({'test':True})
+ t = template.Template('<h1>{% if test %}Yes{% endif %}</h1>')
+ expected = '<h1>Yes</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testSingleIfStatementFalse(self):
+ "An if statement should not display its contents if the test is false"
+ c = template.Context({'test':False})
+ t = template.Template('<h1>{% if test %}Should not see this{% endif %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNestedIfStatementTrueThenTrue(self):
+ "Nested if statements should work properly (case 1)"
+ c = template.Context({'test1':True, 'test2':True})
+ t = template.Template('<h1>{% if test1 %} First {% if test2 %} Second {% endif %} First again {% endif %}</h1>')
+ expected = '<h1> First Second First again </h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNestedIfStatementTrueThenFalse(self):
+ "Nested if statements should work properly (case 2)"
+ c = template.Context({'test1':True, 'test2':False})
+ t = template.Template('<h1>{% if test1 %} First {% if test2 %} Second {% endif %} First again {% endif %}</h1>')
+ expected = '<h1> First First again </h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNestedIfStatementFalseThenTrue(self):
+ "Nested if statements should work properly (case 3)"
+ c = template.Context({'test1':False, 'test2':True})
+ t = template.Template('<h1>{% if test1 %} First {% if test2 %} Second {% endif %} First again {% endif %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNestedIfStatementFalseThenFalse(self):
+ "Nested if statements should work properly (case 4)"
+ c = template.Context({'test1':False, 'test2':False})
+ t = template.Template('<h1>{% if test1 %} First {% if test2 %} Second {% endif %} First again {% endif %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testElseIfTrue(self):
+ "An else statement should not execute if the test evaluates to true"
+ c = template.Context({'test':True})
+ t = template.Template('<h1>{% if test %}Correct{% else %}Incorrect{% endif %}</h1>')
+ expected = '<h1>Correct</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testElseIfFalse(self):
+ "An else statement should execute if the test evaluates to false"
+ c = template.Context({'test':False})
+ t = template.Template('<h1>{% if test %}Incorrect{% else %}Correct{% endif %}</h1>')
+ expected = '<h1>Correct</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNonClosedIfTag(self):
+ "Raise TemplateSyntaxError for non-closed 'if' tags"
+ c = template.Context({'test':True})
+ t = '<h1>{% if test %}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testNonexistentTest(self):
+ "Fail silently when an if statement accesses a nonexistent test"
+ c = template.Context({'var':'value'})
+ t = template.Template('<h1>{% if nonexistent %}Hello{% endif %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testIfTagNoArgs(self):
+ "If statements must have one argument (case 1)"
+ t = '<h1>{% if %}Hello{% endif %}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testIfTagManyArgs(self):
+ "If statements must have one argument (case 2)"
+ t = '<h1>{% if multiple tests %}Hello{% endif %}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testAttributeAccessInIfNode(self):
+ "An if node should resolve a variable's attributes before checking it as a test"
+ class AClass: pass
+ obj = AClass()
+ obj.article = AClass()
+ obj.article.section = AClass()
+ obj.article.section.title = 'Headline'
+ c = template.Context({'obj':obj})
+ t = template.Template('<h1>{% if obj.article.section.title %}Hello{% endif %}</h1>')
+ expected = '<h1>Hello</h1>'
+ self.assertEqual(expected, t.render(c))
+ t = template.Template('<h1>{% if obj.article.section.not_here %}Hello{% endif %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testIfNot(self):
+ "If statements supports 'not' as an optional argument"
+ t = template.Template('{% if not a %}Not a{% endif %}')
+ c = template.Context({'a': False})
+ expected = 'Not a'
+ self.assertEqual(expected, t.render(c))
+ c['a'] = True
+ expected = ''
+ self.assertEqual(expected, t.render(c))
+
+ def testIfOr(self):
+ "If statements support 'or'"
+ t = template.Template('{% if a or b %}Hello{% endif %}')
+ c = template.Context({'a': False, 'b': True})
+ expected = 'Hello'
+ self.assertEqual(expected, t.render(c))
+ c['b'] = False
+ expected = ''
+ self.assertEqual(expected, t.render(c))
+
+ def testIfOrNot(self):
+ "If statements support 'or' clauses with optional 'not's"
+ t = template.Template('{% if a or not b or c%}Hello{% endif %}')
+ c = template.Context({'a': False, 'b': False, 'c': False})
+ expected = 'Hello'
+ self.assertEqual(expected, t.render(c))
+ c['b'] = True
+ expected = ''
+ self.assertEqual(expected, t.render(c))
+
+class ForLoopCheck(unittest.TestCase):
+
+ def testNormalForLoop(self):
+ "A for loop should work as expected, given one or more values"
+ c = template.Context({'pieces': ('1', '2', '3')})
+ t = template.Template('<h1>{% for piece in pieces %}{{ piece }}{% endfor %}</h1>')
+ expected = '<h1>123</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testBlankForLoop(self):
+ "A for loop should work as expected, given an empty list"
+ c = template.Context({'pieces': []})
+ t = template.Template('<h1>{% for piece in pieces %}{{ piece }}{% endfor %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testInvalidForTagFourWords(self):
+ "Raise TemplateSyntaxError if a 'for' statement is not exactly 4 words"
+ t = '<h1>{% for article %}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testInvalidForTagThirdWord(self):
+ "Raise TemplateSyntaxError if 3rd word in a 'for' statement isn't 'in'"
+ t = '<h1>{% for article NOTIN blah %}{% endfor %}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testNonClosedForTag(self):
+ "Raise TemplateSyntaxError for non-closed 'for' tags"
+ t = '<h1>{% for i in numbers %}{{ i }}</h1>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testNonexistentVariable1(self):
+ "Fail silently in loops with nonexistent variables in defn"
+ c = template.Context({'var':'value'})
+ t = template.Template('<h1>{% for i in nonexistent %}<p>{{ var }}</p>{% endfor %}</h1>')
+ expected = '<h1></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testNonexistentVariable2(self):
+ "Raise TemplateSyntaxError in loops with nonexistent variables in loop"
+ c = template.Context({'set':('val1', 'val2')})
+ t = template.Template('<h1>{% for i in set %}<p>{{ nonexistent }}</p>{% endfor %}</h1>')
+ expected = '<h1><p></p><p></p></h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testAttributeAccessInForNode(self):
+ "A for node should resolve a variable's attributes before looping through it"
+ c = template.Context({'article': {'authors':('Simon', 'Adrian')}})
+ t = template.Template('<p>{% for i in article.authors %}{{ i }}{% endfor %}</p>')
+ self.assertEqual('<p>SimonAdrian</p>', t.render(c))
+ t = template.Template('<p>{% for i in article.nonexistent %}{{ i }}{% endfor %}</p>')
+ self.assertEqual('<p></p>', t.render(c))
+
+ def testForLoopFirst(self):
+ "A for loop's 'first' variable should work as expected"
+ c = template.Context({'pieces': ('1', '2', '3')})
+ t = template.Template('<h1>{% for piece in pieces %}{% if forloop.first %}<h2>First</h2>{% endif %}{{ piece }}{% endfor %}</h1>')
+ expected = '<h1><h2>First</h2>123</h1>'
+ self.assertEqual(expected, t.render(c))
+
+ def testForLoopLast(self):
+ "A for loop's 'last' variable should work as expected"
+ c = template.Context({'pieces': ('1', '2', '3')})
+ t = template.Template('<h1>{% for piece in pieces %}{% if forloop.last %}<h2>Last</h2>{% endif %}{{ piece }}{% endfor %}</h1>')
+ expected = '<h1>12<h2>Last</h2>3</h1>'
+ self.assertEqual(expected, t.render(c))
+
+class CycleNodeCheck(unittest.TestCase):
+
+ def testNormalUsage(self):
+ "A cycle tag should work as expected"
+ c = template.Context({'set':range(10)})
+ t = template.Template('{% for i in set %}{% cycle red, green %}-{{ i }} {% endfor %}')
+ expected = 'red-0 green-1 red-2 green-3 red-4 green-5 red-6 green-7 red-8 green-9 '
+ self.assertEqual(expected, t.render(c))
+
+ def testNoArguments(self):
+ "Raise TemplateSyntaxError in cycle tags with no arguments"
+ t = '{% cycle %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testOneArgument(self):
+ "Raise TemplateSyntaxError in cycle tags with only one argument"
+ t = '{% cycle hello %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testExtraInitialSpaces(self):
+ "Extra spaces around cycle tags and their arguments should be ignored"
+ c = template.Context({'set':range(5)})
+ t = template.Template('{% for i in set %}{% cycle red, green %}{% endfor %}')
+ expected = 'redgreenredgreenred'
+ self.assertEqual(expected, t.render(c))
+
+class TemplateTagNodeCheck(unittest.TestCase):
+
+ def testNormalUsage(self):
+ "A templatetag tag should work as expected"
+ c = template.Context()
+ t = template.Template('{% templatetag openblock %}{% templatetag closeblock %}{% templatetag openvariable %}{% templatetag closevariable %}')
+ expected = '{%%}{{}}'
+ self.assertEqual(expected, t.render(c))
+
+ def testNoArguments(self):
+ "Raise TemplateSyntaxError in templatetag tags with no arguments"
+ t = '{% templatetag %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testTwoArguments(self):
+ "Raise TemplateSyntaxError in templatetag tags with more than one argument"
+ t = '{% templatetag hello goodbye %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+ t = '{% templatetag hello goodbye helloagain %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+ def testBadArgument(self):
+ "Raise TemplateSyntaxError in templatetag tags with invalid arguments"
+ t = '{% templatetag hello %}'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+class PluginFilterCheck(unittest.TestCase):
+
+ def custom_filter(self, value, arg):
+ "Temporary filter used to verify the filter plugin system is working"
+ return "_%s_%s_" % (value, arg)
+
+ def testPluginFilter(self):
+ "Plugin support allows for custom filters"
+ template.register_filter('unittest', self.custom_filter, True)
+ c = template.Context({'var':'value'})
+ t = template.Template('<body>{{ var|unittest:"hello" }}</body>')
+ expected = '<body>_value_hello_</body>'
+ self.assertEqual(expected, t.render(c))
+ template.unregister_filter('unittest')
+
+ def testUnregisterPluginFilter(self):
+ "Plugin support allows custom filters to be unregistered"
+ template.register_filter('unittest', self.custom_filter, True)
+ c = template.Context({'var':'value'})
+ t = template.Template('<body>{{ var|unittest:"hello" }}</body>')
+ rendered = t.render(c) # should run with no exception
+ template.unregister_filter('unittest')
+
+class PluginTagCheck(unittest.TestCase):
+
+ class CustomNode(template.Node):
+ "Prints argument"
+ def __init__(self, arg):
+ self.arg = arg
+
+ def render(self, context):
+ return '_%s_' % self.arg
+
+ def do_custom_node(self, parser, token):
+ "Handle the 'unittest' custom tag"
+ bits = token.contents.split()
+ return self.CustomNode(bits[1])
+
+ def testPluginTag(self):
+ "Plugin support allows for custom tags"
+ template.register_tag('unittest', self.do_custom_node)
+ c = template.Context({})
+ t = template.Template('<body>{% unittest hello %}</body>')
+ expected = '<body>_hello_</body>'
+ self.assertEqual(expected, t.render(c))
+ template.unregister_tag('unittest')
+
+ def testUnregisterPluginTag(self):
+ "Plugin support allows custom tags to be unregistered"
+ template.register_tag('unittest', self.do_custom_node)
+ c = template.Context({})
+ t = template.Template('<body>{% unittest hello %}</body>')
+ rendered = t.render(c) # should run with no exception
+ del(t)
+ template.unregister_tag('unittest')
+ t = '<body>{% unittest hello %}</body>'
+ self.assertRaises(template.TemplateSyntaxError, template.Template, t)
+
+class ContextUsageCheck(unittest.TestCase):
+
+ def testVariableContext2(self):
+ "Variables should fall through additional block-level contexts"
+ c = template.Context({'global':'out', 'set': ('1', '2', '3')})
+ t = template.Template('<body><h1>{{ global }}</h1>{% for i in set %}<p>{{ i }} {{ global }}</p>{% endfor %}</body>')
+ expected = '<body><h1>out</h1><p>1 out</p><p>2 out</p><p>3 out</p></body>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableContext2(self):
+ "Variables set within a block statement override like-named variables within their scope"
+ c = template.Context({'i':'out', 'set': ('1', '2', '3')})
+ t = template.Template('<body><h1>{{ i }}</h1>{% for i in set %}<p>{{ i }}</p>{% endfor %}{{ i }}</body>')
+ expected = '<body><h1>out</h1><p>1</p><p>2</p><p>3</p>out</body>'
+ self.assertEqual(expected, t.render(c))
+
+ def testVariableContextDelete(self):
+ "Variables can be deleted from the current context"
+ c = template.Context({'a':'first', 'b':'second'})
+ del c['a']
+ self.assertEqual(c.__repr__(), template.Context({'b':'second'}).__repr__())
+
+ def testInvalidVariableContextDelete(self):
+ "Raise KeyError if code tries to delete a variable that doesn't exist in the current context"
+ c = template.Context({'a':'first'})
+ self.assertRaises(KeyError, c.__delitem__, 'b')
+
+class AdvancedUsageCheck(unittest.TestCase):
+
+ def testIfInsideFor(self):
+ "An if statement should be executed repeatedly inside a for statement"
+ c = template.Context({'set':(True, False, True, True, False)})
+ t = template.Template('<ul>{% for i in set %}{% if i %}<li>1</li>{% endif %}{% endfor %}</ul>')
+ expected = '<ul><li>1</li><li>1</li><li>1</li></ul>'
+ self.assertEqual(expected, t.render(c))
+
+ def testIfElseInsideFor(self):
+ "An if/else statement should be executed repeatedly inside a for statement"
+ c = template.Context({'set':(True, False, True, True, False)})
+ t = template.Template('<ul>{% for i in set %}<li>{% if i %}1{% else %}0{% endif %}</li>{% endfor %}</ul>')
+ expected = '<ul><li>1</li><li>0</li><li>1</li><li>1</li><li>0</li></ul>'
+ self.assertEqual(expected, t.render(c))
+
+ def testForInsideIf_True(self):
+ "A for loop inside an if statement should be executed if the test=true"
+ c = template.Context({'test':True, 'set':('1', '2', '3')})
+ t = template.Template('<body>{% if test %}<ul>{% for i in set %}<li>{{ i }}</li>{% endfor %}</ul>{% endif %}</body>')
+ expected = '<body><ul><li>1</li><li>2</li><li>3</li></ul></body>'
+ self.assertEqual(expected, t.render(c))
+
+ def testForInsideIf_False(self):
+ "A for loop inside an if statement shouldn't be executed if the test=false"
+ c = template.Context({'test':False, 'set':('1', '2', '3')})
+ t = template.Template('<body>{% if test %}<ul>{% for i in set %}<li>{{ i }}</li>{% endfor %}</ul>{% endif %}</body>')
+ expected = '<body></body>'
+ self.assertEqual(expected, t.render(c))
+
+ def testForInsideIfInsideFor(self):
+ "A for loop inside an if statement inside a for loop should work properly"
+ c = template.Context({'set1': (True, False, False, False, True), 'set2': ('1', '2', '3')})
+ t = template.Template('<body>{% for i in set1 %}{% if i %}{% for j in set2 %}{{ j }}{% endfor %}{% endif %}{% endfor %}</body>')
+ expected = '<body>123123</body>'
+ self.assertEqual(expected, t.render(c))
+
+ def testMultipleRendersWhenCompiled(self):
+ "A template can render multiple contexts without having to be recompiled"
+ t = template.Template('<body>{% for i in set1 %}{% if i %}{% for j in set2 %}{{ j }}{% endfor %}{% endif %}{% endfor %}</body>')
+ c = template.Context({'set1': (True, False, False, False, False), 'set2': ('1', '2', '3')})
+ self.assertEqual('<body>123</body>', t.render(c))
+ c = template.Context({'set1': (True, True, False, False, False), 'set2': ('1', '2', '3')})
+ self.assertEqual('<body>123123</body>', t.render(c))
+ c = template.Context({'set1': (True, True, True, False, False), 'set2': ('1', '2', '3')})
+ self.assertEqual('<body>123123123</body>', t.render(c))
+ c = template.Context({'set1': (True, True, True, True, False), 'set2': ('1', '2', '3')})
+ self.assertEqual('<body>123123123123</body>', t.render(c))
+ c = template.Context({'set1': (True, True, True, True, True), 'set2': ('1', '2', '3')})
+ self.assertEqual('<body>123123123123123</body>', t.render(c))
+
+def tests():
+ s = unittest.TestLoader().loadTestsFromName(__name__)
+ unittest.TextTestRunner(verbosity=0).run(s)
+
+if __name__ == "__main__":
+ tests()
diff --git a/django/utils/__init__.py b/django/utils/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/utils/__init__.py
diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py
new file mode 100644
index 0000000000..c1cb6193f5
--- /dev/null
+++ b/django/utils/datastructures.py
@@ -0,0 +1,171 @@
+class MergeDict:
+ """
+ A simple class for creating new "virtual" dictionaries that actualy look
+ up values in more than one dictionary, passed in the constructor.
+ """
+ def __init__(self, *dicts):
+ self.dicts = dicts
+
+ def __getitem__(self, key):
+ for dict in self.dicts:
+ try:
+ return dict[key]
+ except KeyError:
+ pass
+ raise KeyError
+
+ def get(self, key, default):
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def getlist(self, key):
+ for dict in self.dicts:
+ try:
+ return dict.getlist(key)
+ except KeyError:
+ pass
+ raise KeyError
+
+ def items(self):
+ item_list = []
+ for dict in self.dicts:
+ item_list.extend(dict.items())
+ return item_list
+
+ def has_key(self, key):
+ for dict in self.dicts:
+ if dict.has_key(key):
+ return True
+ return False
+
+class MultiValueDictKeyError(KeyError):
+ pass
+
+class MultiValueDict:
+ """
+ A dictionary-like class customized to deal with multiple values for the same key.
+
+ >>> d = MultiValueDict({'name': ['Adrian', 'Simon'], 'position': ['Developer']})
+ >>> d['name']
+ 'Simon'
+ >>> d.getlist('name')
+ ['Adrian', 'Simon']
+ >>> d.get('lastname', 'nonexistent')
+ 'nonexistent'
+ >>> d.setlist('lastname', ['Holovaty', 'Willison'])
+
+ This class exists to solve the irritating problem raised by cgi.parse_qs,
+ which returns a list for every key, even though most Web forms submit
+ single name-value pairs.
+ """
+ def __init__(self, key_to_list_mapping=None):
+ self.data = key_to_list_mapping or {}
+
+ def __repr__(self):
+ return repr(self.data)
+
+ def __getitem__(self, key):
+ "Returns the data value for this key; raises KeyError if not found"
+ if self.data.has_key(key):
+ try:
+ return self.data[key][-1] # in case of duplicates, use last value ([-1])
+ except IndexError:
+ return []
+ raise MultiValueDictKeyError, "Key '%s' not found in MultiValueDict %s" % (key, self.data)
+
+ def __setitem__(self, key, value):
+ self.data[key] = [value]
+
+ def __len__(self):
+ return len(self.data)
+
+ def get(self, key, default):
+ "Returns the default value if the requested data doesn't exist"
+ try:
+ val = self[key]
+ except (KeyError, IndexError):
+ return default
+ if val == []:
+ return default
+ return val
+
+ def getlist(self, key):
+ "Returns an empty list if the requested data doesn't exist"
+ try:
+ return self.data[key]
+ except KeyError:
+ return []
+
+ def setlist(self, key, list_):
+ self.data[key] = list_
+
+ def appendlist(self, key, item):
+ "Appends an item to the internal list associated with key"
+ try:
+ self.data[key].append(item)
+ except KeyError:
+ self.data[key] = [item]
+
+ def has_key(self, key):
+ return self.data.has_key(key)
+
+ def items(self):
+ # we don't just return self.data.items() here, because we want to use
+ # self.__getitem__() to access the values as *strings*, not lists
+ return [(key, self[key]) for key in self.data.keys()]
+
+ def keys(self):
+ return self.data.keys()
+
+ def update(self, other_dict):
+ if isinstance(other_dict, MultiValueDict):
+ for key, value_list in other_dict.data.items():
+ self.data.setdefault(key, []).extend(value_list)
+ elif type(other_dict) == type({}):
+ for key, value in other_dict.items():
+ self.data.setdefault(key, []).append(value)
+ else:
+ raise ValueError, "MultiValueDict.update() takes either a MultiValueDict or dictionary"
+
+ def copy(self):
+ "Returns a copy of this object"
+ import copy
+ cp = copy.deepcopy(self)
+ return cp
+
+class DotExpandedDict(dict):
+ """
+ A special dictionary constructor that takes a dictionary in which the keys
+ may contain dots to specify inner dictionaries. It's confusing, but this
+ example should make sense.
+
+ >>> d = DotExpandedDict({'person.1.firstname': ['Simon'],
+ 'person.1.lastname': ['Willison'],
+ 'person.2.firstname': ['Adrian'],
+ 'person.2.lastname': ['Holovaty']})
+ >>> d
+ {'person': {'1': {'lastname': ['Willison'], 'firstname': ['Simon']},
+ '2': {'lastname': ['Holovaty'], 'firstname': ['Adrian']}}}
+ >>> d['person']
+ {'1': {'firstname': ['Simon'], 'lastname': ['Willison'],
+ '2': {'firstname': ['Adrian'], 'lastname': ['Holovaty']}
+ >>> d['person']['1']
+ {'firstname': ['Simon'], 'lastname': ['Willison']}
+
+ # Gotcha: Results are unpredictable if the dots are "uneven":
+ >>> DotExpandedDict({'c.1': 2, 'c.2': 3, 'c': 1})
+ >>> {'c': 1}
+ """
+ def __init__(self, key_to_list_mapping):
+ for k, v in key_to_list_mapping.items():
+ current = self
+ bits = k.split('.')
+ for bit in bits[:-1]:
+ current = current.setdefault(bit, {})
+ # Now assign value to current position
+ try:
+ current[bits[-1]] = v
+ except TypeError: # Special-case if current isn't a dict.
+ current = {bits[-1]: v}
diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py
new file mode 100644
index 0000000000..9913f01a57
--- /dev/null
+++ b/django/utils/dateformat.py
@@ -0,0 +1,317 @@
+"""
+PHP date() style date formatting
+See http://www.php.net/date for format strings
+
+Usage:
+>>> import datetime
+>>> d = datetime.datetime.now()
+>>> df = DateFormat(d)
+>>> print df.format('jS F Y H:i')
+7th October 2003 11:39
+>>>
+"""
+
+from calendar import isleap
+from dates import MONTHS, MONTHS_AP, WEEKDAYS
+
+class DateFormat:
+ year_days = [None, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
+
+ def __init__(self, d):
+ self.date = d
+
+ def a(self):
+ "'a.m.' or 'p.m.'"
+ if self.date.hour > 11:
+ return 'p.m.'
+ return 'a.m.'
+
+ def A(self):
+ "'AM' or 'PM'"
+ if self.date.hour > 11:
+ return 'PM'
+ return 'AM'
+
+ def B(self):
+ "Swatch Internet time"
+ raise NotImplementedError
+
+ def d(self):
+ "Day of the month, 2 digits with leading zeros; i.e. '01' to '31'"
+ return '%02d' % self.date.day
+
+ def D(self):
+ "Day of the week, textual, 3 letters; e.g. 'Fri'"
+ return WEEKDAYS[self.date.weekday()][0:3]
+
+ def f(self):
+ """
+ Time, in 12-hour hours and minutes, with minutes left off if they're zero.
+ Examples: '1', '1:30', '2:05', '2'
+ Proprietary extension.
+ """
+ if self.date.minute == 0:
+ return self.g()
+ return '%s:%s' % (self.g(), self.i())
+
+ def F(self):
+ "Month, textual, long; e.g. 'January'"
+ return MONTHS[self.date.month]
+
+ def g(self):
+ "Hour, 12-hour format without leading zeros; i.e. '1' to '12'"
+ if self.date.hour == 0:
+ return 12
+ if self.date.hour > 12:
+ return self.date.hour - 12
+ return self.date.hour
+
+ def G(self):
+ "Hour, 24-hour format without leading zeros; i.e. '0' to '23'"
+ return self.date.hour
+
+ def h(self):
+ "Hour, 12-hour format; i.e. '01' to '12'"
+ return '%02d' % self.g()
+
+ def H(self):
+ "Hour, 24-hour format; i.e. '00' to '23'"
+ return '%02d' % self.G()
+
+ def i(self):
+ "Minutes; i.e. '00' to '59'"
+ return '%02d' % self.date.minute
+
+ def I(self):
+ "'1' if Daylight Savings Time, '0' otherwise."
+ raise NotImplementedError
+
+ def j(self):
+ "Day of the month without leading zeros; i.e. '1' to '31'"
+ return self.date.day
+
+ def l(self):
+ "Day of the week, textual, long; e.g. 'Friday'"
+ return WEEKDAYS[self.date.weekday()]
+
+ def L(self):
+ "Boolean for whether it is a leap year; i.e. True or False"
+ return isleap(self.date.year)
+
+ def m(self):
+ "Month; i.e. '01' to '12'"
+ return '%02d' % self.date.month
+
+ def M(self):
+ "Month, textual, 3 letters; e.g. 'Jan'"
+ return MONTHS[self.date.month][0:3]
+
+ def n(self):
+ "Month without leading zeros; i.e. '1' to '12'"
+ return self.date.month
+
+ def N(self):
+ "Month abbreviation in Associated Press style. Proprietary extension."
+ return MONTHS_AP[self.date.month]
+
+ def O(self):
+ "Difference to Greenwich time in hours; e.g. '+0200'"
+ raise NotImplementedError
+
+ def P(self):
+ """
+ Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
+ if they're zero and the strings 'midnight' and 'noon' if appropriate.
+ Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
+ Proprietary extension.
+ """
+ if self.date.minute == 0 and self.date.hour == 0:
+ return 'midnight'
+ if self.date.minute == 0 and self.date.hour == 12:
+ return 'noon'
+ return '%s %s' % (self.f(), self.a())
+
+ def r(self):
+ "RFC 822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'"
+ raise NotImplementedError
+
+ def s(self):
+ "Seconds; i.e. '00' to '59'"
+ return '%02d' % self.date.second
+
+ def S(self):
+ "English ordinal suffix for the day of the month, 2 characters; i.e. 'st', 'nd', 'rd' or 'th'"
+ if self.date.day in (11, 12, 13): # Special case
+ return 'th'
+ last = self.date.day % 10
+ if last == 1:
+ return 'st'
+ if last == 2:
+ return 'nd'
+ if last == 3:
+ return 'rd'
+ return 'th'
+
+ def t(self):
+ "Number of days in the given month; i.e. '28' to '31'"
+ raise NotImplementedError
+
+ def T(self):
+ "Time zone of this machine; e.g. 'EST' or 'MDT'"
+ raise NotImplementedError
+
+ def U(self):
+ "Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)"
+ raise NotImplementedError
+
+ def w(self):
+ "Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)"
+ weekday = self.date.weekday()
+ if weekday == 0:
+ return 6
+ return weekday - 1
+
+ def W(self):
+ "ISO-8601 week number of year, weeks starting on Monday"
+ # Algorithm from http://www.personal.ecu.edu/mccartyr/ISOwdALG.txt
+ week_number = None
+ jan1_weekday = self.date.replace(month=1, day=1).weekday() + 1
+ weekday = self.date.weekday() + 1
+ day_of_year = self.z()
+ if day_of_year <= (8 - jan1_weekday) and jan1_weekday > 4:
+ if jan1_weekday == 5 or (jan1_weekday == 6 and isleap(self.date.year-1)):
+ week_number = 53
+ else:
+ week_number = 52
+ else:
+ if isleap(self.date.year):
+ i = 366
+ else:
+ i = 365
+ if (i - day_of_year) < (4 - weekday):
+ week_number = 1
+ else:
+ j = day_of_year + (7 - weekday) + (jan1_weekday - 1)
+ week_number = j / 7
+ if jan1_weekday > 4:
+ week_number -= 1
+ return week_number
+
+ def Y(self):
+ "Year, 4 digits; e.g. '1999'"
+ return self.date.year
+
+ def y(self):
+ "Year, 2 digits; e.g. '99'"
+ return str(self.date.year)[2:]
+
+ def z(self):
+ "Day of the year; i.e. '0' to '365'"
+ doy = self.year_days[self.date.month] + self.date.day
+ if self.L() and self.date.month > 2:
+ doy += 1
+ return doy
+
+ def Z(self):
+ """Time zone offset in seconds (i.e. '-43200' to '43200'). The offset
+ for timezones west of UTC is always negative, and for those east of UTC
+ is always positive."""
+ raise NotImplementedError
+
+ def format(self, formatstr):
+ result = ''
+ for char in formatstr:
+ try:
+ result += str(getattr(self, char)())
+ except AttributeError:
+ result += char
+ return result
+
+class TimeFormat:
+ def __init__(self, t):
+ self.time = t
+
+ def a(self):
+ "'a.m.' or 'p.m.'"
+ if self.time.hour > 11:
+ return 'p.m.'
+ else:
+ return 'a.m.'
+
+ def A(self):
+ "'AM' or 'PM'"
+ return self.a().upper()
+
+ def B(self):
+ "Swatch Internet time"
+ raise NotImplementedError
+
+ def f(self):
+ """
+ Time, in 12-hour hours and minutes, with minutes left off if they're zero.
+ Examples: '1', '1:30', '2:05', '2'
+ Proprietary extension.
+ """
+ if self.time.minute == 0:
+ return self.g()
+ return '%s:%s' % (self.g(), self.i())
+
+ def g(self):
+ "Hour, 12-hour format without leading zeros; i.e. '1' to '12'"
+ if self.time.hour == 0:
+ return 12
+ if self.time.hour > 12:
+ return self.time.hour - 12
+ return self.time.hour
+
+ def G(self):
+ "Hour, 24-hour format without leading zeros; i.e. '0' to '23'"
+ return self.time.hour
+
+ def h(self):
+ "Hour, 12-hour format; i.e. '01' to '12'"
+ return '%02d' % self.g()
+
+ def H(self):
+ "Hour, 24-hour format; i.e. '00' to '23'"
+ return '%02d' % self.G()
+
+ def i(self):
+ "Minutes; i.e. '00' to '59'"
+ return '%02d' % self.time.minute
+
+ def P(self):
+ """
+ Time, in 12-hour hours, minutes and 'a.m.'/'p.m.', with minutes left off
+ if they're zero and the strings 'midnight' and 'noon' if appropriate.
+ Examples: '1 a.m.', '1:30 p.m.', 'midnight', 'noon', '12:30 p.m.'
+ Proprietary extension.
+ """
+ if self.time.minute == 0 and self.time.hour == 0:
+ return 'midnight'
+ if self.time.minute == 0 and self.time.hour == 12:
+ return 'noon'
+ return '%s %s' % (self.f(), self.a())
+
+ def s(self, s):
+ "Seconds; i.e. '00' to '59'"
+ return '%02d' % self.time.second
+
+ def format(self, formatstr):
+ result = ''
+ for char in formatstr:
+ try:
+ result += str(getattr(self, char)())
+ except AttributeError:
+ result += char
+ return result
+
+def format(value, format_string):
+ "Convenience function"
+ df = DateFormat(value)
+ return df.format(format_string)
+
+def time_format(value, format_string):
+ "Convenience function"
+ tf = TimeFormat(value)
+ return tf.format(format_string)
diff --git a/django/utils/dates.py b/django/utils/dates.py
new file mode 100644
index 0000000000..2ae0cc1a6e
--- /dev/null
+++ b/django/utils/dates.py
@@ -0,0 +1,27 @@
+"Commonly-used date structures"
+
+WEEKDAYS = {
+ 0:'Monday', 1:'Tuesday', 2:'Wednesday', 3:'Thursday', 4:'Friday',
+ 5:'Saturday', 6:'Sunday'
+}
+WEEKDAYS_REV = {
+ 'monday':0, 'tuesday':1, 'wednesday':2, 'thursday':3, 'friday':4,
+ 'saturday':5, 'sunday':6
+}
+MONTHS = {
+ 1:'January', 2:'February', 3:'March', 4:'April', 5:'May', 6:'June',
+ 7:'July', 8:'August', 9:'September', 10:'October', 11:'November',
+ 12:'December'
+}
+MONTHS_3 = {
+ 1:'jan', 2:'feb', 3:'mar', 4:'apr', 5:'may', 6:'jun', 7:'jul', 8:'aug',
+ 9:'sep', 10:'oct', 11:'nov', 12:'dec'
+}
+MONTHS_3_REV = {
+ 'jan':1, 'feb':2, 'mar':3, 'apr':4, 'may':5, 'jun':6, 'jul':7, 'aug':8,
+ 'sep':9, 'oct':10, 'nov':11, 'dec':12
+}
+MONTHS_AP = { # month names in Associated Press style
+ 1:'Jan.', 2:'Feb.', 3:'March', 4:'April', 5:'May', 6:'June', 7:'July',
+ 8:'Aug.', 9:'Sept.', 10:'Oct.', 11:'Nov.', 12:'Dec.'
+}
diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py
new file mode 100644
index 0000000000..dc5dd31fe4
--- /dev/null
+++ b/django/utils/feedgenerator.py
@@ -0,0 +1,152 @@
+"""
+Syndication feed generation library -- used for generating RSS, etc.
+
+By Adrian Holovaty
+Released under the Python license
+
+Sample usage:
+
+>>> feed = feedgenerator.Rss201rev2Feed(
+... title=u"Poynter E-Media Tidbits",
+... link=u"http://www.poynter.org/column.asp?id=31",
+... description=u"A group weblog by the sharpest minds in online media/journalism/publishing.",
+... language=u"en",
+... )
+>>> feed.add_item(title="Hello", link=u"http://www.holovaty.com/test/", description="Testing.")
+>>> fp = open('test.rss', 'w')
+>>> feed.write(fp, 'utf-8')
+>>> fp.close()
+
+For definitions of the different versions of RSS, see:
+http://diveintomark.org/archives/2004/02/04/incompatible-rss
+"""
+
+from django.utils.xmlutils import SimplerXMLGenerator
+
+class SyndicationFeed:
+ "Base class for all syndication feeds. Subclasses should provide write()"
+ def __init__(self, title, link, description, language=None):
+ self.feed_info = {
+ 'title': title,
+ 'link': link,
+ 'description': description,
+ 'language': language,
+ }
+ self.items = []
+
+ def add_item(self, title, link, description, author_email=None,
+ author_name=None, pubdate=None, comments=None, unique_id=None,
+ enclosure=None):
+ """
+ Adds an item to the feed. All args are expected to be Python Unicode
+ objects except pubdate, which is a datetime.datetime object, and
+ enclosure, which is an instance of the Enclosure class.
+ """
+ self.items.append({
+ 'title': title,
+ 'link': link,
+ 'description': description,
+ 'author_email': author_email,
+ 'author_name': author_name,
+ 'pubdate': pubdate,
+ 'comments': comments,
+ 'unique_id': unique_id,
+ 'enclosure': enclosure,
+ })
+
+ def num_items(self):
+ return len(self.items)
+
+ def write(self, outfile, encoding):
+ """
+ Outputs the feed in the given encoding to outfile, which is a file-like
+ object. Subclasses should override this.
+ """
+ raise NotImplementedError
+
+ def writeString(self, encoding):
+ """
+ Returns the feed in the given encoding as a string.
+ """
+ from StringIO import StringIO
+ s = StringIO()
+ self.write(s, encoding)
+ return s.getvalue()
+
+class Enclosure:
+ "Represents an RSS enclosure"
+ def __init__(self, url, length, mime_type):
+ "All args are expected to be Python Unicode objects"
+ self.url, self.length, self.mime_type = url, length, mime_type
+
+class RssFeed(SyndicationFeed):
+ def write(self, outfile, encoding):
+ handler = SimplerXMLGenerator(outfile, encoding)
+ handler.startDocument()
+ self.writeRssElement(handler)
+ self.writeChannelElement(handler)
+ for item in self.items:
+ self.writeRssItem(handler, item)
+ self.endChannelElement(handler)
+ self.endRssElement(handler)
+
+ def writeRssElement(self, handler):
+ "Adds the <rss> element to handler, taking care of versioning, etc."
+ raise NotImplementedError
+
+ def endRssElement(self, handler):
+ "Ends the <rss> element."
+ handler.endElement(u"rss")
+
+ def writeChannelElement(self, handler):
+ handler.startElement(u"channel", {})
+ handler.addQuickElement(u"title", self.feed_info['title'], {})
+ handler.addQuickElement(u"link", self.feed_info['link'], {})
+ handler.addQuickElement(u"description", self.feed_info['description'], {})
+ if self.feed_info['language'] is not None:
+ handler.addQuickElement(u"language", self.feed_info['language'], {})
+
+ def endChannelElement(self, handler):
+ handler.endElement(u"channel")
+
+class RssUserland091Feed(RssFeed):
+ def startRssElement(self, handler):
+ handler.startElement(u"rss", {u"version": u"0.91"})
+
+ def writeRssItem(self, handler, item):
+ handler.startElement(u"item", {})
+ handler.addQuickElement(u"title", item['title'], {})
+ handler.addQuickElement(u"link", item['link'], {})
+ if item['description'] is not None:
+ handler.addQuickElement(u"description", item['description'], {})
+ handler.endElement(u"item")
+
+class Rss201rev2Feed(RssFeed):
+ # Spec: http://blogs.law.harvard.edu/tech/rss
+ def writeRssElement(self, handler):
+ handler.startElement(u"rss", {u"version": u"2.0"})
+
+ def writeRssItem(self, handler, item):
+ handler.startElement(u"item", {})
+ handler.addQuickElement(u"title", item['title'], {})
+ handler.addQuickElement(u"link", item['link'], {})
+ if item['description'] is not None:
+ handler.addQuickElement(u"description", item['description'], {})
+ if item['author_email'] is not None and item['author_name'] is not None:
+ handler.addQuickElement(u"author", u"%s (%s)" % \
+ (item['author_email'], item['author_name']), {})
+ if item['pubdate'] is not None:
+ handler.addQuickElement(u"pubDate", item['pubdate'].strftime('%a, %d %b %Y %H:%M:%S %Z'), {})
+ if item['comments'] is not None:
+ handler.addQuickElement(u"comments", item['comments'], {})
+ if item['unique_id'] is not None:
+ handler.addQuickElement(u"guid", item['unique_id'], {})
+ if item['enclosure'] is not None:
+ handler.addQuickElement(u"enclosure", '',
+ {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
+ u"type": item['enclosure'].mime_type})
+ handler.endElement(u"item")
+
+# This isolates the decision of what the system default is, so calling code can
+# do "feedgenerator.DefaultRssFeed" instead of "feedgenerator.Rss201rev2Feed".
+DefaultRssFeed = Rss201rev2Feed
diff --git a/django/utils/html.py b/django/utils/html.py
new file mode 100644
index 0000000000..13ee6e742a
--- /dev/null
+++ b/django/utils/html.py
@@ -0,0 +1,110 @@
+"Useful HTML utilities suitable for global use by World Online projects."
+
+import re, string
+
+# Configuration for urlize() function
+LEADING_PUNCTUATION = ['(', '<', '&lt;']
+TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;']
+
+# list of possible strings used for bullets in bulleted lists
+DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
+
+UNENCODED_AMPERSANDS_RE = re.compile(r'&(?!(\w+|#\d+);)')
+WORD_SPLIT_RE = re.compile(r'(\s+)')
+PUNCTUATION_RE = re.compile('^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % \
+ ('|'.join([re.escape(p) for p in LEADING_PUNCTUATION]),
+ '|'.join([re.escape(p) for p in TRAILING_PUNCTUATION])))
+SIMPLE_EMAIL_RE = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
+LINK_TARGET_ATTRIBUTE = re.compile(r'(<a [^>]*?)target=[^\s>]+')
+HTML_GUNK = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
+HARD_CODED_BULLETS = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(d) for d in DOTS]), re.DOTALL)
+TRAILING_EMPTY_CONTENT = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
+
+def escape(html):
+ "Returns the given HTML with ampersands, quotes and carets encoded"
+ if not isinstance(html, basestring):
+ html = str(html)
+ return html.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
+
+def linebreaks(value):
+ "Converts newlines into <p> and <br />s"
+ value = re.sub(r'\r\n|\r|\n', '\n', value) # normalize newlines
+ paras = re.split('\n{2,}', value)
+ paras = ['<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
+ return '\n\n'.join(paras)
+
+def strip_tags(value):
+ "Returns the given HTML with all tags stripped"
+ return re.sub(r'<[^>]*?>', '', value)
+
+def strip_entities(value):
+ "Returns the given HTML with all entities (&something;) stripped"
+ return re.sub(r'&(?:\w+|#\d);', '', value)
+
+def fix_ampersands(value):
+ "Returns the given HTML with all unencoded ampersands encoded correctly"
+ return UNENCODED_AMPERSANDS_RE.sub('&amp;', value)
+
+def urlize(text, trim_url_limit=None, nofollow=False):
+ """
+ Converts any URLs in text into clickable links. Works on http://, https:// and
+ www. links. Links can have trailing punctuation (periods, commas, close-parens)
+ and leading punctuation (opening parens) and it'll still do the right thing.
+
+ If trim_url_limit is not None, the URLs in link text will be limited to
+ trim_url_limit characters.
+
+ If nofollow is True, the URLs in link text will get a rel="nofollow" attribute.
+ """
+ trim_url = lambda x, limit=trim_url_limit: limit is not None and (x[:limit] + (len(x) >=limit and '...' or '')) or x
+ words = WORD_SPLIT_RE.split(text)
+ nofollow_attr = nofollow and ' rel="nofollow"' or ''
+ for i, word in enumerate(words):
+ match = PUNCTUATION_RE.match(word)
+ if match:
+ lead, middle, trail = match.groups()
+ if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \
+ len(middle) > 0 and middle[0] in string.letters + string.digits and \
+ (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
+ middle = '<a href="http://%s"%s>%s</a>' % (middle, nofollow_attr, trim_url(middle))
+ if middle.startswith('http://') or middle.startswith('https://'):
+ middle = '<a href="%s"%s>%s</a>' % (middle, nofollow_attr, trim_url(middle))
+ if '@' in middle and not middle.startswith('www.') and not ':' in middle \
+ and SIMPLE_EMAIL_RE.match(middle):
+ middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
+ if lead + middle + trail != word:
+ words[i] = lead + middle + trail
+ return ''.join(words)
+
+def clean_html(text):
+ """
+ Cleans the given HTML. Specifically, it does the following:
+ * Converts <b> and <i> to <strong> and <em>.
+ * Encodes all ampersands correctly.
+ * Removes all "target" attributes from <a> tags.
+ * Removes extraneous HTML, such as presentational tags that open and
+ immediately close and <br clear="all">.
+ * Converts hard-coded bullets into HTML unordered lists.
+ * Removes stuff like "<p>&nbsp;&nbsp;</p>", but only if it's at the
+ bottom of the text.
+ """
+ from django.utils.text import normalize_newlines
+ text = normalize_newlines(text)
+ text = re.sub(r'<(/?)\s*b\s*>', '<\\1strong>', text)
+ text = re.sub(r'<(/?)\s*i\s*>', '<\\1em>', text)
+ text = fix_ampersands(text)
+ # Remove all target="" attributes from <a> tags.
+ text = LINK_TARGET_ATTRIBUTE.sub('\\1', text)
+ # Trim stupid HTML such as <br clear="all">.
+ text = HTML_GUNK.sub('', text)
+ # Convert hard-coded bullets into HTML unordered lists.
+ def replace_p_tags(match):
+ s = match.group().replace('</p>', '</li>')
+ for d in DOTS:
+ s = s.replace('<p>%s' % d, '<li>')
+ return '<ul>\n%s\n</ul>' % s
+ text = HARD_CODED_BULLETS.sub(replace_p_tags, text)
+ # Remove stuff like "<p>&nbsp;&nbsp;</p>", but only if it's at the bottom of the text.
+ text = TRAILING_EMPTY_CONTENT.sub('', text)
+ return text
+
diff --git a/django/utils/httpwrappers.py b/django/utils/httpwrappers.py
new file mode 100644
index 0000000000..513a5bc0d7
--- /dev/null
+++ b/django/utils/httpwrappers.py
@@ -0,0 +1,319 @@
+from Cookie import SimpleCookie
+from pprint import pformat
+import datastructures
+
+DEFAULT_MIME_TYPE = 'text/html'
+
+class HttpRequest(object): # needs to be new-style class because subclasses define "property"s
+ "A basic HTTP request"
+ def __init__(self):
+ self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {}
+ self.path = ''
+
+ def __repr__(self):
+ return '<HttpRequest\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \
+ (pformat(self.GET), pformat(self.POST), pformat(self.COOKIES),
+ pformat(self.META))
+
+ def __getitem__(self, key):
+ for d in (self.POST, self.GET):
+ if d.has_key(key):
+ return d[key]
+ raise KeyError, "%s not found in either POST or GET" % key
+
+ def get_full_path(self):
+ return ''
+
+class ModPythonRequest(HttpRequest):
+ def __init__(self, req):
+ self._req = req
+ self.path = req.uri
+
+ def __repr__(self):
+ return '<ModPythonRequest\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \
+ (pformat(self.GET), pformat(self.POST), pformat(self.COOKIES),
+ pformat(self.META))
+
+ def get_full_path(self):
+ return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '')
+
+ 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 = parse_file_upload(self._req)
+ else:
+ self._post, self._files = QueryDict(self._req.read()), datastructures.MultiValueDict()
+
+ def _get_request(self):
+ if not hasattr(self, '_request'):
+ self._request = datastructures.MergeDict(self.POST, self.GET)
+ return self._request
+
+ def _get_get(self):
+ if not hasattr(self, '_get'):
+ self._get = QueryDict(self._req.args)
+ return self._get
+
+ def _set_get(self, get):
+ self._get = get
+
+ def _get_post(self):
+ if not hasattr(self, '_post'):
+ self._load_post_and_files()
+ return self._post
+
+ def _set_post(self, post):
+ self._post = post
+
+ def _get_cookies(self):
+ if not hasattr(self, '_cookies'):
+ self._cookies = parse_cookie(self._req.headers_in.get('cookie', ''))
+ return self._cookies
+
+ def _set_cookies(self, cookies):
+ self._cookies = cookies
+
+ def _get_files(self):
+ if not hasattr(self, '_files'):
+ self._load_post_and_files()
+ return self._files
+
+ def _get_meta(self):
+ "Lazy loader that returns self.META dictionary"
+ if not hasattr(self, '_meta'):
+ self._meta = {
+ 'AUTH_TYPE': self._req.ap_auth_type,
+ 'CONTENT_LENGTH': self._req.clength, # This may be wrong
+ 'CONTENT_TYPE': self._req.content_type, # This may be wrong
+ 'GATEWAY_INTERFACE': 'CGI/1.1',
+ 'PATH_INFO': self._req.path_info,
+ 'PATH_TRANSLATED': None, # Not supported
+ 'QUERY_STRING': self._req.args,
+ 'REMOTE_ADDR': self._req.connection.remote_ip,
+ 'REMOTE_HOST': None, # DNS lookups not supported
+ 'REMOTE_IDENT': self._req.connection.remote_logname,
+ 'REMOTE_USER': self._req.user,
+ 'REQUEST_METHOD': self._req.method,
+ 'SCRIPT_NAME': None, # Not supported
+ 'SERVER_NAME': self._req.server.server_hostname,
+ 'SERVER_PORT': self._req.server.port,
+ 'SERVER_PROTOCOL': self._req.protocol,
+ 'SERVER_SOFTWARE': 'mod_python'
+ }
+ for key, value in self._req.headers_in.items():
+ key = 'HTTP_' + key.upper().replace('-', '_')
+ self._meta[key] = value
+ return self._meta
+
+ GET = property(_get_get, _set_get)
+ POST = property(_get_post, _set_post)
+ COOKIES = property(_get_cookies, _set_cookies)
+ FILES = property(_get_files)
+ META = property(_get_meta)
+ REQUEST = property(_get_request)
+
+def parse_file_upload(req):
+ "Returns a tuple of (POST MultiValueDict, FILES MultiValueDict), given a mod_python req object"
+ import email, email.Message
+ from cgi import parse_header
+ raw_message = '\r\n'.join(['%s:%s' % pair for pair in req.headers_in.items()])
+ raw_message += '\r\n\r\n' + req.read()
+ msg = email.message_from_string(raw_message)
+ POST = datastructures.MultiValueDict()
+ FILES = datastructures.MultiValueDict()
+ for submessage in msg.get_payload():
+ if isinstance(submessage, email.Message.Message):
+ name_dict = parse_header(submessage['Content-Disposition'])[1]
+ # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads
+ # or {'name': 'blah'} for POST fields
+ # We assume all uploaded files have a 'filename' set.
+ if name_dict.has_key('filename'):
+ assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported"
+ if not name_dict['filename'].strip():
+ continue
+ # IE submits the full path, so trim everything but the basename.
+ # (We can't use os.path.basename because it expects Linux paths.)
+ filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:]
+ FILES.appendlist(name_dict['name'], {
+ 'filename': filename,
+ 'content-type': (submessage.has_key('Content-Type') and submessage['Content-Type'] or None),
+ 'content': submessage.get_payload(),
+ })
+ else:
+ POST.appendlist(name_dict['name'], submessage.get_payload())
+ return POST, FILES
+
+class QueryDict(datastructures.MultiValueDict):
+ """A specialized MultiValueDict that takes a query string when initialized.
+ This is immutable unless you create a copy of it."""
+ def __init__(self, query_string):
+ try:
+ from mod_python.util import parse_qsl
+ except ImportError:
+ from cgi import parse_qsl
+ if not query_string:
+ self.data = {}
+ self._keys = []
+ else:
+ self.data = {}
+ self._keys = []
+ for name, value in parse_qsl(query_string, True): # keep_blank_values=True
+ if name in self.data:
+ self.data[name].append(value)
+ else:
+ self.data[name] = [value]
+ if name not in self._keys:
+ self._keys.append(name)
+ self._mutable = False
+
+ def __setitem__(self, key, value):
+ if not self._mutable:
+ raise AttributeError, "This QueryDict instance is immutable"
+ else:
+ self.data[key] = [value]
+ if not key in self._keys:
+ self._keys.append(key)
+
+ def setlist(self, key, list_):
+ if not self._mutable:
+ raise AttributeError, "This QueryDict instance is immutable"
+ else:
+ self.data[key] = list_
+ if not key in self._keys:
+ self._keys.append(key)
+
+ def copy(self):
+ "Returns a mutable copy of this object"
+ cp = datastructures.MultiValueDict.copy(self)
+ cp._mutable = True
+ return cp
+
+ def assert_synchronized(self):
+ assert(len(self._keys) == len(self.data.keys())), \
+ "QueryDict data structure is out of sync: %s %s" % (str(self._keys), str(self.data))
+
+ def items(self):
+ "Respect order preserved by self._keys"
+ self.assert_synchronized()
+ items = []
+ for key in self._keys:
+ if key in self.data:
+ items.append((key, self.data[key][0]))
+ return items
+
+ def keys(self):
+ self.assert_synchronized()
+ return self._keys
+
+def parse_cookie(cookie):
+ if cookie == '':
+ return {}
+ c = SimpleCookie()
+ c.load(cookie)
+ cookiedict = {}
+ for key in c.keys():
+ cookiedict[key] = c.get(key).value
+ return cookiedict
+
+class HttpResponse:
+ "A basic HTTP response, with content and dictionary-accessed headers"
+ def __init__(self, content='', mimetype=DEFAULT_MIME_TYPE):
+ self.content = content
+ self.headers = {'Content-Type':mimetype}
+ self.cookies = SimpleCookie()
+ self.status_code = 200
+
+ def __str__(self):
+ "Full HTTP message, including headers"
+ return '\n'.join(['%s: %s' % (key, value)
+ for key, value in self.headers.items()]) \
+ + '\n\n' + self.content
+
+ def __setitem__(self, header, value):
+ self.headers[header] = value
+
+ def __delitem__(self, header):
+ try:
+ del self.headers[header]
+ except KeyError:
+ pass
+
+ def __getitem__(self, header):
+ return self.headers[header]
+
+ def has_header(self, header):
+ "Case-insensitive check for a header"
+ header = header.lower()
+ for key in self.headers.keys():
+ if key.lower() == header:
+ return True
+ return False
+
+ def set_cookie(self, key, value='', max_age=None, path='/', domain=None, secure=None):
+ self.cookies[key] = value
+ for var in ('max_age', 'path', 'domain', 'secure'):
+ val = locals()[var]
+ if val is not None:
+ self.cookies[key][var.replace('_', '-')] = val
+
+ def get_content_as_string(self, encoding):
+ """
+ Returns the content as a string, encoding it from a Unicode object if
+ necessary.
+ """
+ if isinstance(self.content, unicode):
+ return self.content.encode(encoding)
+ return self.content
+
+ # The remaining methods partially implement the file-like object interface.
+ # See http://docs.python.org/lib/bltin-file-objects.html
+ def write(self, content):
+ self.content += content
+
+ def flush(self):
+ pass
+
+ def tell(self):
+ return len(self.content)
+
+class HttpResponseRedirect(HttpResponse):
+ def __init__(self, redirect_to):
+ HttpResponse.__init__(self)
+ self['Location'] = redirect_to
+ self.status_code = 302
+
+class HttpResponseNotModified(HttpResponse):
+ def __init__(self):
+ HttpResponse.__init__(self)
+ self.status_code = 304
+
+class HttpResponseNotFound(HttpResponse):
+ def __init__(self, content='', mimetype=DEFAULT_MIME_TYPE):
+ HttpResponse.__init__(self, content, mimetype)
+ self.status_code = 404
+
+class HttpResponseForbidden(HttpResponse):
+ def __init__(self, content='', mimetype=DEFAULT_MIME_TYPE):
+ HttpResponse.__init__(self, content, mimetype)
+ self.status_code = 403
+
+class HttpResponseGone(HttpResponse):
+ def __init__(self, content='', mimetype=DEFAULT_MIME_TYPE):
+ HttpResponse.__init__(self, content, mimetype)
+ self.status_code = 410
+
+class HttpResponseServerError(HttpResponse):
+ def __init__(self, content='', mimetype=DEFAULT_MIME_TYPE):
+ HttpResponse.__init__(self, content, mimetype)
+ self.status_code = 500
+
+def populate_apache_request(http_response, mod_python_req):
+ "Populates the mod_python request object with an HttpResponse"
+ mod_python_req.content_type = http_response['Content-Type'] or DEFAULT_MIME_TYPE
+ del http_response['Content-Type']
+ if http_response.cookies:
+ mod_python_req.headers_out['Set-Cookie'] = http_response.cookies.output(header='')
+ for key, value in http_response.headers.items():
+ mod_python_req.headers_out[key] = value
+ mod_python_req.status = http_response.status_code
+ mod_python_req.write(http_response.get_content_as_string('utf-8'))
diff --git a/django/utils/images.py b/django/utils/images.py
new file mode 100644
index 0000000000..75424f16a2
--- /dev/null
+++ b/django/utils/images.py
@@ -0,0 +1,22 @@
+"""
+Utility functions for handling images.
+
+Requires PIL, as you might imagine.
+"""
+
+import ImageFile
+
+def get_image_dimensions(path):
+ """Returns the (width, height) of an image at a given path."""
+ p = ImageFile.Parser()
+ fp = open(path)
+ while 1:
+ data = fp.read(1024)
+ if not data:
+ break
+ p.feed(data)
+ if p.image:
+ return p.image.size
+ break
+ fp.close()
+ return None
diff --git a/django/utils/stopwords.py b/django/utils/stopwords.py
new file mode 100644
index 0000000000..dea5660413
--- /dev/null
+++ b/django/utils/stopwords.py
@@ -0,0 +1,42 @@
+# Performance note: I benchmarked this code using a set instead of
+# a list for the stopwords and was surprised to find that the list
+# performed /better/ than the set - maybe because it's only a small
+# list.
+
+stopwords = '''
+i
+a
+an
+are
+as
+at
+be
+by
+for
+from
+how
+in
+is
+it
+of
+on
+or
+that
+the
+this
+to
+was
+what
+when
+where
+'''.split()
+
+def strip_stopwords(sentence):
+ "Removes stopwords - also normalizes whitespace"
+ words = sentence.split()
+ sentence = []
+ for word in words:
+ if word.lower() not in stopwords:
+ sentence.append(word)
+ return ' '.join(sentence)
+
diff --git a/django/utils/text.py b/django/utils/text.py
new file mode 100644
index 0000000000..cb9e9454d7
--- /dev/null
+++ b/django/utils/text.py
@@ -0,0 +1,108 @@
+import re
+
+def wrap(text, width):
+ """
+ A word-wrap function that preserves existing line breaks and most spaces in
+ the text. Expects that existing line breaks are posix newlines (\n).
+ See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
+ """
+ return reduce(lambda line, word, width=width: '%s%s%s' %
+ (line,
+ ' \n'[(len(line[line.rfind('\n')+1:])
+ + len(word.split('\n',1)[0]
+ ) >= width)],
+ word),
+ text.split(' ')
+ )
+
+def truncate_words(s, num):
+ "Truncates a string after a certain number of words."
+ length = int(num)
+ words = s.split()
+ if len(words) > length:
+ words = words[:length]
+ if not words[-1].endswith('...'):
+ words.append('...')
+ return ' '.join(words)
+
+def get_valid_filename(s):
+ """
+ Returns the given string converted to a string that can be used for a clean
+ filename. Specifically, leading and trailing spaces are removed; other
+ spaces are converted to underscores; and all non-filename-safe characters
+ are removed.
+ >>> get_valid_filename("john's portrait in 2004.jpg")
+ 'johns_portrait_in_2004.jpg'
+ """
+ s = s.strip().replace(' ', '_')
+ return re.sub(r'[^-A-Za-z0-9_.]', '', s)
+
+def fix_microsoft_characters(s):
+ """
+ Converts Microsoft proprietary characters (e.g. smart quotes, em-dashes)
+ to sane characters
+ """
+ # Sources:
+ # http://stsdas.stsci.edu/bps/pythontalk8.html
+ # http://www.waider.ie/hacks/workshop/perl/rss-fetch.pl
+ # http://www.fourmilab.ch/webtools/demoroniser/
+ return s
+ s = s.replace('\x91', "'")
+ s = s.replace('\x92', "'")
+ s = s.replace('\x93', '"')
+ s = s.replace('\x94', '"')
+ s = s.replace('\xd2', '"')
+ s = s.replace('\xd3', '"')
+ s = s.replace('\xd5', "'")
+ s = s.replace('\xad', '--')
+ s = s.replace('\xd0', '--')
+ s = s.replace('\xd1', '--')
+ s = s.replace('\xe2\x80\x98', "'") # weird single quote (open)
+ s = s.replace('\xe2\x80\x99', "'") # weird single quote (close)
+ s = s.replace('\xe2\x80\x9c', '"') # weird double quote (open)
+ s = s.replace('\xe2\x80\x9d', '"') # weird double quote (close)
+ s = s.replace('\xe2\x81\x84', '/')
+ s = s.replace('\xe2\x80\xa6', '...')
+ s = s.replace('\xe2\x80\x94', '--')
+ return s
+
+def get_text_list(list_, last_word='or'):
+ """
+ >>> get_text_list(['a', 'b', 'c', 'd'])
+ 'a, b, c or d'
+ >>> get_text_list(['a', 'b', 'c'], 'and')
+ 'a, b and c'
+ >>> get_text_list(['a', 'b'], 'and')
+ 'a and b'
+ >>> get_text_list(['a'])
+ 'a'
+ >>> get_text_list([])
+ ''
+ """
+ if len(list_) == 0: return ''
+ if len(list_) == 1: return list_[0]
+ return '%s %s %s' % (', '.join([i for i in list_][:-1]), last_word, list_[-1])
+
+def normalize_newlines(text):
+ return re.sub(r'\r\n|\r|\n', '\n', text)
+
+def recapitalize(text):
+ "Recapitalizes text, placing caps after end-of-sentence punctuation."
+ capwords = 'I Jayhawk Jayhawks Lawrence Kansas KS'.split()
+ text = text.lower()
+ capsRE = re.compile(r'(?:^|(?<=[\.\?\!] ))([a-z])')
+ text = capsRE.sub(lambda x: x.group(1).upper(), text)
+ for capword in capwords:
+ capwordRE = re.compile(r'\b%s\b' % capword, re.I)
+ text = capwordRE.sub(capword, text)
+ return text
+
+def phone2numeric(phone):
+ "Converts a phone number with letters into its numeric equivalent."
+ letters = re.compile(r'[A-PR-Y]', re.I)
+ char2number = lambda m: {'a': '2', 'c': '2', 'b': '2', 'e': '3',
+ 'd': '3', 'g': '4', 'f': '3', 'i': '4', 'h': '4', 'k': '5',
+ 'j': '5', 'm': '6', 'l': '5', 'o': '6', 'n': '6', 'p': '7',
+ 's': '7', 'r': '7', 'u': '8', 't': '8', 'w': '9', 'v': '8',
+ 'y': '9', 'x': '9'}.get(m.group(0).lower())
+ return letters.sub(char2number, phone)
diff --git a/django/utils/timesince.py b/django/utils/timesince.py
new file mode 100644
index 0000000000..c11cef0342
--- /dev/null
+++ b/django/utils/timesince.py
@@ -0,0 +1,46 @@
+import time, math, datetime
+
+def timesince(d, now=None):
+ """
+ Takes a datetime object, returns the time between then and now
+ as a nicely formatted string, e.g "10 minutes"
+ Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since
+ """
+ original = time.mktime(d.timetuple())
+ chunks = (
+ (60 * 60 * 24 * 365, 'year'),
+ (60 * 60 * 24 * 30, 'month'),
+ (60 * 60 * 24, 'day'),
+ (60 * 60, 'hour'),
+ (60, 'minute')
+ )
+ if not now:
+ now = time.time()
+ since = now - original
+ # Crazy iteration syntax because we need i to be current index
+ for i, (seconds, name) in zip(range(len(chunks)), chunks):
+ count = math.floor(since / seconds)
+ if count != 0:
+ break
+ if count == 1:
+ s = '1 %s' % name
+ else:
+ s = '%d %ss' % (count, name)
+ if i + 1 < len(chunks):
+ # Now get the second item
+ seconds2, name2 = chunks[i + 1]
+ count2 = math.floor((since - (seconds * count)) / seconds2)
+ if count2 != 0:
+ if count2 == 1:
+ s += ', 1 %s' % name2
+ else:
+ s += ', %d %ss' % (count2, name2)
+ return s
+
+def timeuntil(d):
+ """
+ Like timesince, but returns a string measuring the time until
+ the given time.
+ """
+ now = datetime.datetime.now()
+ return timesince(now, time.mktime(d.timetuple()))
diff --git a/django/utils/xmlutils.py b/django/utils/xmlutils.py
new file mode 100644
index 0000000000..6638573857
--- /dev/null
+++ b/django/utils/xmlutils.py
@@ -0,0 +1,13 @@
+"""
+Utilities for XML generation/parsing.
+"""
+
+from xml.sax.saxutils import XMLGenerator
+
+class SimplerXMLGenerator(XMLGenerator):
+ def addQuickElement(self, name, contents=None, attrs={}):
+ "Convenience method for adding an element with no children"
+ self.startElement(name, attrs)
+ if contents is not None:
+ self.characters(contents)
+ self.endElement(name)
diff --git a/django/views/__init__.py b/django/views/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/__init__.py
diff --git a/django/views/admin/__init__.py b/django/views/admin/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/admin/__init__.py
diff --git a/django/views/admin/doc.py b/django/views/admin/doc.py
new file mode 100644
index 0000000000..7f4e686a82
--- /dev/null
+++ b/django/views/admin/doc.py
@@ -0,0 +1,328 @@
+import os
+import re
+import inspect
+from django.core import meta
+from django import templatetags
+from django.conf import settings
+from django.models.core import sites
+from django.views.decorators.cache import cache_page
+from django.core.extensions import CMSContext as Context
+from django.core.exceptions import Http404, ViewDoesNotExist
+from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
+from django.core import template, template_loader, defaulttags, defaultfilters, urlresolvers
+try:
+ from django.parts.admin import doc
+except ImportError:
+ doc = None
+
+# Exclude methods starting with these strings from documentation
+MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_')
+
+def doc_index(request):
+ if not doc:
+ return missing_docutils_page(request)
+
+ t = template_loader.get_template('doc/index')
+ c = Context(request, {})
+ return HttpResponse(t.render(c))
+
+def bookmarklets(request):
+ t = template_loader.get_template('doc/bookmarklets')
+ c = Context(request, {
+ 'admin_url' : "%s://%s" % (os.environ.get('HTTPS') == 'on' and 'https' or 'http', request.META['HTTP_HOST']),
+ })
+ return HttpResponse(t.render(c))
+
+def template_tag_index(request):
+ if not doc:
+ return missing_docutils_page(request)
+
+ # We have to jump through some hoops with registered_tags to make sure
+ # they don't get messed up by loading outside tagsets
+ saved_tagset = template.registered_tags.copy(), template.registered_filters.copy()
+ load_all_installed_template_libraries()
+
+ # Gather docs
+ tags = []
+ for tagname in template.registered_tags:
+ title, body, metadata = doc.parse_docstring(template.registered_tags[tagname].__doc__)
+ if title:
+ title = doc.parse_rst(title, 'tag', 'tag:' + tagname)
+ if body:
+ body = doc.parse_rst(body, 'tag', 'tag:' + tagname)
+ for key in metadata:
+ metadata[key] = doc.parse_rst(metadata[key], 'tag', 'tag:' + tagname)
+ library = template.registered_tags[tagname].__module__.split('.')[-1]
+ if library == 'template_loader' or library == 'defaulttags':
+ library = None
+ tags.append({
+ 'name' : tagname,
+ 'title' : title,
+ 'body' : body,
+ 'meta' : metadata,
+ 'library' : library,
+ })
+
+ # Fix registered_tags
+ template.registered_tags, template.registered_filters = saved_tagset
+
+ t = template_loader.get_template('doc/template_tag_index')
+ c = Context(request, {
+ 'tags' : tags,
+ })
+ return HttpResponse(t.render(c))
+template_tag_index = cache_page(template_tag_index, 15*60)
+
+def template_filter_index(request):
+ if not doc:
+ return missing_docutils_page(request)
+
+ saved_tagset = template.registered_tags.copy(), template.registered_filters.copy()
+ load_all_installed_template_libraries()
+
+ filters = []
+ for filtername in template.registered_filters:
+ title, body, metadata = doc.parse_docstring(template.registered_filters[filtername][0].__doc__)
+ if title:
+ title = doc.parse_rst(title, 'filter', 'filter:' + filtername)
+ if body:
+ body = doc.parse_rst(body, 'filter', 'filter:' + filtername)
+ for key in metadata:
+ metadata[key] = doc.parse_rst(metadata[key], 'filter', 'filter:' + filtername)
+ metadata['AcceptsArgument'] = template.registered_filters[filtername][1]
+ library = template.registered_filters[filtername][0].__module__.split('.')[-1]
+ if library == 'template_loader' or library == 'defaultfilters':
+ library = None
+ filters.append({
+ 'name' : filtername,
+ 'title' : title,
+ 'body' : body,
+ 'meta' : metadata,
+ 'library' : library,
+ })
+
+ template.registered_tags, template.registered_filters = saved_tagset
+
+ t = template_loader.get_template('doc/template_filter_index')
+ c = Context(request, {
+ 'filters' : filters,
+ })
+ return HttpResponse(t.render(c))
+template_filter_index = cache_page(template_filter_index, 15*60)
+
+def view_index(request):
+ if not doc:
+ return missing_docutils_page(request)
+
+ views = []
+ for site_settings_module in settings.ADMIN_FOR:
+ settings_mod = __import__(site_settings_module, '', '', [''])
+ urlconf = __import__(settings_mod.ROOT_URLCONF, '', '', [''])
+ view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns)
+ for (func, regex) in view_functions:
+ title, body, metadata = doc.parse_docstring(func.__doc__)
+ if title:
+ title = doc.parse_rst(title, 'view', 'view:' + func.__name__)
+ views.append({
+ 'name' : func.__name__,
+ 'module' : func.__module__,
+ 'title' : title,
+ 'site_id': settings_mod.SITE_ID,
+ 'site' : sites.get_object(id__exact=settings_mod.SITE_ID),
+ 'url' : simplify_regex(regex),
+ })
+ t = template_loader.get_template('doc/view_index')
+ c = Context(request, {
+ 'views' : views,
+ })
+ return HttpResponse(t.render(c))
+view_index = cache_page(view_index, 15*60)
+
+def view_detail(request, view):
+ if not doc:
+ return missing_docutils_page(request)
+
+ mod, func = urlresolvers.get_mod_func(view)
+ try:
+ view_func = getattr(__import__(mod, '', '', ['']), func)
+ except (ImportError, AttributeError):
+ raise Http404
+ title, body, metadata = doc.parse_docstring(view_func.__doc__)
+ if title:
+ title = doc.parse_rst(title, 'view', 'view:' + view)
+ if body:
+ body = doc.parse_rst(body, 'view', 'view:' + view)
+ for key in metadata:
+ metadata[key] = doc.parse_rst(metadata[key], 'view', 'view:' + view)
+ t = template_loader.get_template('doc/view_detail')
+ c = Context(request, {
+ 'name' : view,
+ 'summary' : title,
+ 'body' : body,
+ 'meta' : metadata,
+ })
+ return HttpResponse(t.render(c))
+
+def model_index(request):
+ if not doc:
+ 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,
+ })
+ t = template_loader.get_template('doc/model_index')
+ c = Context(request, {
+ 'models' : models,
+ })
+ return HttpResponse(t.render(c))
+
+def model_detail(request, model):
+ if not doc:
+ return missing_docutils_page(request)
+
+ try:
+ model = meta.get_app(model)
+ except ImportError:
+ raise Http404
+ opts = model.Klass._meta
+
+ # Gather fields/field descriptions
+ fields = []
+ for field in opts.fields:
+ fields.append({
+ 'name' : field.name,
+ 'data_type': get_readable_field_data_type(field),
+ 'verbose' : field.verbose_name,
+ 'help' : field.help_text,
+ })
+ for func_name, func in model.Klass.__dict__.items():
+ if callable(func) and len(inspect.getargspec(func)[0]) == 0:
+ try:
+ for exclude in MODEL_METHODS_EXCLUDE:
+ if func_name.startswith(exclude):
+ raise StopIteration
+ except StopIteration:
+ continue
+ verbose = func.__doc__
+ if verbose:
+ verbose = doc.parse_rst(doc.trim_docstring(verbose), 'model', 'model:' + opts.module_name)
+ fields.append({
+ 'name' : func_name,
+ 'data_type' : get_return_data_type(func_name),
+ 'verbose' : verbose,
+ })
+
+ t = template_loader.get_template('doc/model_detail')
+ c = Context(request, {
+ 'name' : '%s.%s' % (opts.app_label, opts.module_name),
+ 'summary' : "Fields on %s objects" % opts.verbose_name,
+ 'fields' : fields,
+ })
+ return HttpResponse(t.render(c))
+
+####################
+# Helper functions #
+####################
+
+def missing_docutils_page(request):
+ """Display an error message for people without docutils"""
+ t = template_loader.get_template('doc/missing_docutils')
+ c = Context(request, {})
+ return HttpResponse(t.render(c))
+
+def load_all_installed_template_libraries():
+ # Clear out and reload default tags
+ template.registered_tags.clear()
+ reload(defaulttags)
+ reload(template_loader) # template_loader defines the block/extends tags
+
+ # Load any template tag libraries from installed apps
+ for e in templatetags.__path__:
+ libraries = [os.path.splitext(p)[0] for p in os.listdir(e) if p.endswith('.py') and p[0].isalpha()]
+ for lib in libraries:
+ try:
+ mod = defaulttags.LoadNode.load_taglib(lib)
+ reload(mod)
+ except ImportError:
+ pass
+
+def get_return_data_type(func_name):
+ """Return a somewhat-helpful data type given a function name"""
+ if func_name.startswith('get_'):
+ if func_name.endswith('_list'):
+ return 'List'
+ elif func_name.endswith('_count'):
+ return 'Integer'
+ return ''
+
+# Maps Field objects to their human-readable data 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_TYPE_MAPPING = {
+ 'AutoField' : 'Integer',
+ 'BooleanField' : 'Boolean (Either True or False)',
+ 'CharField' : 'String (up to %(maxlength)s)',
+ 'CommaSeparatedIntegerField': 'Comma-separated integers',
+ 'DateField' : 'Date (without time)',
+ 'DateTimeField' : 'Date (with time)',
+ 'EmailField' : 'E-mail address',
+ 'FileField' : 'File path',
+ 'FloatField' : 'Decimal number',
+ 'ImageField' : 'File path',
+ 'IntegerField' : 'Integer',
+ 'IPAddressField' : 'IP address',
+ 'ManyToManyField' : '',
+ 'NullBooleanField' : 'Boolean (Either True, False or None)',
+ 'PhoneNumberField' : 'Phone number',
+ 'PositiveIntegerField' : 'Integer',
+ 'PositiveSmallIntegerField' : 'Integer',
+ 'SlugField' : 'String (up to 50)',
+ 'SmallIntegerField' : 'Integer',
+ 'TextField' : 'Text',
+ 'TimeField' : 'Time',
+ 'URLField' : 'URL',
+ 'USStateField' : 'U.S. state (two uppercase letters)',
+ 'XMLField' : 'XML text',
+}
+
+def get_readable_field_data_type(field):
+ return DATA_TYPE_MAPPING[field.__class__.__name__] % field.__dict__
+
+def extract_views_from_urlpatterns(urlpatterns, base=''):
+ """
+ Return a list of views from a list of urlpatterns.
+
+ Each object in the returned list is a two-tuple: (view_func, regex)
+ """
+ views = []
+ for p in urlpatterns:
+ if hasattr(p, 'get_callback'):
+ try:
+ views.append((p.get_callback(), base + p.regex.pattern))
+ except ViewDoesNotExist:
+ continue
+ elif hasattr(p, 'get_url_patterns'):
+ views.extend(extract_views_from_urlpatterns(p.get_url_patterns(), base + p.regex.pattern))
+ else:
+ 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+>).+?\)')
+
+def simplify_regex(pattern):
+ pattern = named_group_matcher.sub(lambda m: m.group(1), pattern)
+ pattern = pattern.replace('^', '').replace('$', '').replace('?', '').replace('//', '/')
+ if not pattern.startswith('/'):
+ pattern = '/' + pattern
+ return pattern \ No newline at end of file
diff --git a/django/views/admin/main.py b/django/views/admin/main.py
new file mode 100644
index 0000000000..73a1a5fda6
--- /dev/null
+++ b/django/views/admin/main.py
@@ -0,0 +1,1089 @@
+# Generic admin views, with admin templates created dynamically at runtime.
+
+from django.core import formfields, meta, template_loader
+from django.core.exceptions import Http404, ObjectDoesNotExist, PermissionDenied
+from django.core.extensions import CMSContext as Context
+from django.models.auth import log
+from django.utils.html import strip_tags
+from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
+from django.utils.text import get_text_list
+import operator
+
+# Text to display within changelist 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 get_query_string(original_params, new_params={}, remove=[]):
+ """
+ >>> get_query_string({'first_name': 'adrian', 'last_name': 'smith'})
+ '?first_name=adrian&amp;last_name=smith'
+ >>> get_query_string({'first_name': 'adrian', 'last_name': 'smith'}, {'first_name': 'john'})
+ '?first_name=john&amp;last_name=smith'
+ >>> get_query_string({'test': 'yes'}, {'blah': 'no'}, ['te'])
+ '?blah=no'
+ """
+ p = original_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 '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20')
+
+def index(request):
+ t = template_loader.get_template('index')
+ c = Context(request, {'title': 'Site administration'})
+ return HttpResponse(t.render(c))
+
+def logout(request):
+ request.session.delete()
+ t = template_loader.get_template('logged_out')
+ c = Context(request, {
+ 'title': "You're logged out",
+ })
+ return HttpResponse(t.render(c))
+
+def change_list(request, app_label, module_name):
+ from django.core import paginator
+ from django.utils import dateformat
+ from django.utils.dates import MONTHS
+ from django.utils.html import escape
+ import datetime
+
+ # 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
+
+ DEFAULT_RESULTS_PER_PAGE = 100
+
+ ALL_VAR = 'all'
+ ORDER_VAR = 'o'
+ ORDER_TYPE_VAR = 'ot'
+ PAGE_VAR = 'p'
+ SEARCH_VAR = 'q'
+ IS_POPUP_VAR = 'pop'
+
+ mod, opts = _get_mod_opts(app_label, module_name)
+ if not request.user.has_perm(app_label + '.' + opts.get_change_permission()):
+ raise PermissionDenied
+
+ lookup_mod, lookup_opts = mod, opts
+
+ if opts.one_to_one_field:
+ lookup_mod = opts.one_to_one_field.rel.to.get_model_module()
+ lookup_opts = lookup_mod.Klass._meta
+
+ # Get search parameters from the query string.
+ try:
+ page_num = int(request.GET.get(PAGE_VAR, 0))
+ except ValueError:
+ page_num = 0
+ show_all = request.GET.has_key(ALL_VAR)
+ is_popup = request.GET.has_key(IS_POPUP_VAR)
+ params = dict(request.GET.copy())
+ if params.has_key(PAGE_VAR):
+ del params[PAGE_VAR]
+ # For ordering, first check the "ordering" parameter in the admin options,
+ # then check the object's default ordering. Finally, look for manually-
+ # specified ordering from the query string.
+ if lookup_opts.admin.ordering is not None:
+ order_field, order_type = lookup_opts.admin.ordering
+ else:
+ order_field, order_type = lookup_opts.ordering[0]
+ if params.has_key(ORDER_VAR):
+ try:
+ order_key = int(params[ORDER_VAR])
+ try:
+ f = lookup_opts.get_field(lookup_opts.admin.list_display[order_key])
+ except meta.FieldDoesNotExist:
+ pass
+ else:
+ if not isinstance(f.rel, meta.ManyToOne) 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]
+ query = request.GET.get(SEARCH_VAR, '')
+
+ # Prepare the lookup parameters for the API lookup.
+ 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
+ if isinstance(lookup_opts.get_field(order_field).rel, meta.ManyToOne):
+ f = lookup_opts.get_field(order_field)
+ lookup_order_field = '%s.%s' % (f.rel.to.db_table, f.rel.to.ordering[0][0])
+ # Use select_related if one of the list_display options is a field with a
+ # relationship.
+ 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.ManyToOne):
+ lookup_params['select_related'] = True
+ break
+ lookup_params['order_by'] = ((lookup_order_field, order_type),)
+ if lookup_opts.admin.search_fields and query:
+ or_queries = []
+ for bit in query.split():
+ or_query = []
+ for field_name in lookup_opts.admin.search_fields:
+ or_query.append(('%s__icontains' % field_name, bit))
+ or_queries.append(or_query)
+ lookup_params['_or'] = or_queries
+
+ if opts.one_to_one_field:
+ lookup_params.update(opts.one_to_one_field.rel.limit_choices_to)
+
+ # Get the results.
+ try:
+ p = 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:
+ return HttpResponseRedirect(request.path)
+
+ # 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 = p.hits
+ del real_lookup_params
+ result_count = p.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:
+ try:
+ result_list = p.get_page(page_num)
+ except paginator.InvalidPage:
+ result_list = []
+
+ # Calculate filters first, because a CSS class high in the document depends
+ # on whether they are available.
+ filter_template = []
+ if lookup_opts.admin.list_filter and not opts.one_to_one_field:
+ filter_fields = [lookup_opts.get_field(field_name) for field_name in lookup_opts.admin.list_filter]
+ for f in filter_fields:
+ # Many-to-many or many-to-one filter.
+ if f.rel:
+ if isinstance(f, meta.ManyToManyField):
+ lookup_kwarg = '%s__id__exact' % f.name
+ lookup_title = f.rel.to.verbose_name
+ else:
+ lookup_kwarg = '%s__exact' % f.name
+ lookup_title = f.verbose_name
+ lookup_val = request.GET.get(lookup_kwarg, None)
+ lookup_choices = f.rel.to.get_model_module().get_list()
+ if len(lookup_choices) > 1:
+ filter_template.append('<h3>By %s:</h3>\n<ul>\n' % lookup_title)
+ filter_template.append('<li%s><a href="%s">All</a></li>\n' % \
+ ((lookup_val is None and ' class="selected"' or ''),
+ get_query_string(params, {}, [lookup_kwarg])))
+ for val in lookup_choices:
+ filter_template.append('<li%s><a href="%s">%r</a></li>\n' % \
+ ((lookup_val == str(val.id) and ' class="selected"' or ''),
+ get_query_string(params, {lookup_kwarg: val.id}), val))
+ filter_template.append('</ul>\n\n')
+ # Field with choices.
+ elif f.choices:
+ lookup_kwarg = '%s__exact' % f.name
+ lookup_val = request.GET.get(lookup_kwarg, None)
+ filter_template.append('<h3>By %s:</h3><ul>\n' % f.verbose_name)
+ filter_template.append('<li%s><a href="%s">All</a></li>\n' % \
+ ((lookup_val is None and ' class="selected"' or ''),
+ get_query_string(params, {}, [lookup_kwarg])))
+ for k, v in f.choices:
+ filter_template.append('<li%s><a href="%s">%s</a></li>' % \
+ ((str(k) == lookup_val) and ' class="selected"' or '',
+ get_query_string(params, {lookup_kwarg: k}), v))
+ filter_template.append('</ul>\n\n')
+ # Date filter.
+ elif isinstance(f, meta.DateField):
+ today = datetime.date.today()
+ one_week_ago = today - datetime.timedelta(days=7)
+ field_generic = '%s__' % field_name
+ filter_template.append('<h3>By %s:</h3><ul>\n' % f.verbose_name)
+ date_params = dict([(k, v) for k, v in params.items() if k.startswith(field_generic)])
+ today_str = isinstance(f, meta.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d')
+ for title, param_dict in (
+ ('Any date', {}),
+ ('Today', {'%s__year' % f.name: str(today.year), '%s__month' % f.name: str(today.month), '%s__day' % f.name: str(today.day)}),
+ ('Past 7 days', {'%s__gte' % f.name: one_week_ago.strftime('%Y-%m-%d'), '%s__lte' % f.name: today_str}),
+ ('This month', {'%s__year' % f.name: str(today.year), '%s__month' % f.name: str(today.month)}),
+ ('This year', {'%s__year' % f.name: str(today.year)})
+ ):
+ filter_template.append('<li%s><a href="%s">%s</a></li>\n' % \
+ ((date_params == param_dict) and ' class="selected"' or '',
+ get_query_string(params, param_dict, field_generic), title))
+ filter_template.append('</ul>\n\n')
+ elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField):
+ lookup_kwarg = '%s__exact' % f.name
+ lookup_kwarg2 = '%s__isnull' % f.name
+ lookup_val = request.GET.get(lookup_kwarg, None)
+ lookup_val2 = request.GET.get(lookup_kwarg2, None)
+ filter_template.append('<h3>By %s:</h3><ul>\n' % f.verbose_name)
+ for k, v in (('All', None), ('Yes', 'True'), ('No', 'False')):
+ filter_template.append('<li%s><a href="%s">%s</a></li>\n' % \
+ (((lookup_val == v and not lookup_val2) and ' class="selected"' or ''),
+ get_query_string(params, {lookup_kwarg: v}, [lookup_kwarg2]), k))
+ if isinstance(f, meta.NullBooleanField):
+ filter_template.append('<li%s><a href="%s">%s</a></li>\n' % \
+ (((lookup_val2 == 'True') and ' class="selected"' or ''),
+ get_query_string(params, {lookup_kwarg2: 'True'}, [lookup_kwarg]), 'Unknown'))
+ filter_template.append('</ul>\n\n')
+ else:
+ pass # Invalid argument to "list_filter"
+
+ raw_template = ['{% extends "base_site" %}\n']
+ raw_template.append('{% block bodyclass %}change-list{% endblock %}\n')
+ if not is_popup:
+ raw_template.append('{%% block breadcrumbs %%}<div class="breadcrumbs"><a href="../../">Home</a> &rsaquo; %s</div>{%% endblock %%}\n' % meta.capfirst(opts.verbose_name_plural))
+ raw_template.append('{% block coltype %}flex{% endblock %}')
+ raw_template.append('{% block content %}\n')
+ raw_template.append('<div id="content-main">\n')
+ if request.user.has_perm(app_label + '.' + lookup_opts.get_add_permission()):
+ raw_template.append('<ul class="object-tools"><li><a href="add/%s" class="addlink">Add %s</a></li></ul>\n' % ((is_popup and '?_popup=1' or ''), opts.verbose_name))
+ raw_template.append('<div class="module%s" id="changelist">\n' % (filter_template and ' filtered' or ''))
+
+ # Search form.
+ if lookup_opts.admin.search_fields:
+ raw_template.append('<div id="toolbar">\n<form id="changelist-search" action="" method="get">\n')
+ raw_template.append('<label><img src="/m/img/admin/icon_searchbox.png" /></label> ')
+ raw_template.append('<input type="text" size="40" name="%s" value="%s" id="searchbar" /> ' % \
+ (SEARCH_VAR, escape(query)))
+ raw_template.append('<input type="submit" value="Go" /> ')
+ if result_count != full_result_count and not opts.one_to_one_field:
+ raw_template.append('<span class="small quiet">%s result%s (<a href="?">%s total</a>)</span>' % \
+ (result_count, (result_count != 1 and 's' or ''), full_result_count))
+ for k, v in params.items():
+ if k != SEARCH_VAR:
+ raw_template.append('<input type="hidden" name="%s" value="%s" />' % (escape(k), escape(v)))
+ raw_template.append('</form></div>\n')
+ raw_template.append('<script type="text/javascript">document.getElementById("searchbar").focus();</script>')
+
+ # Date-based navigation.
+ if lookup_opts.admin.date_hierarchy:
+ field_name = 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)
+
+ raw_template.append('<div class="xfull">\n<ul class="toplinks">\n')
+ if year_lookup and month_lookup and day_lookup:
+ raw_template.append('<li class="date-back"><a href="%s">&lsaquo; %s %s </a></li>' % \
+ (get_query_string(params, {year_field: year_lookup, month_field: month_lookup}, [field_generic]), MONTHS[int(month_lookup)], year_lookup))
+ raw_template.append('<li>%s %s</li>' % (MONTHS[int(month_lookup)], day_lookup))
+ elif year_lookup and month_lookup:
+ raw_template.append('<li class="date-back"><a href="%s">&lsaquo; %s</a></li>' % \
+ (get_query_string(params, {year_field: year_lookup}, [field_generic]), year_lookup))
+ date_lookup_params = lookup_params.copy()
+ date_lookup_params.update({year_field: year_lookup, month_field: month_lookup})
+ for day in getattr(lookup_mod, 'get_%s_list' % field_name)('day', **date_lookup_params):
+ raw_template.append('<li><a href="%s">%s</a></li>' % \
+ (get_query_string(params, {year_field: year_lookup, month_field: month_lookup, day_field: day.day}, [field_generic]), day.strftime('%B %d')))
+ elif year_lookup:
+ raw_template.append('<li class="date-back"><a href="%s">&lsaquo; All dates</a></li>' % \
+ get_query_string(params, {}, [year_field]))
+ date_lookup_params = lookup_params.copy()
+ date_lookup_params.update({year_field: year_lookup})
+ for month in getattr(lookup_mod, 'get_%s_list' % field_name)('month', **date_lookup_params):
+ raw_template.append('<li><a href="%s">%s %s</a></li>' % \
+ (get_query_string(params, {year_field: year_lookup, month_field: month.month}, [field_generic]), month.strftime('%B'), month.year))
+ else:
+ for year in getattr(lookup_mod, 'get_%s_list' % field_name)('year', **lookup_params):
+ raw_template.append('<li><a href="%s">%s</a></li>\n' % \
+ (get_query_string(params, {year_field: year.year}, [field_generic]), year.year))
+ raw_template.append('</ul><br class="clear" />\n</div>\n')
+
+ # Filters.
+ if filter_template:
+ raw_template.append('<div id="changelist-filter">\n<h2>Filter</h2>\n')
+ raw_template.extend(filter_template)
+ raw_template.append('</div>')
+ del filter_template
+
+ # Result table.
+ if result_list:
+ # Table headers.
+ raw_template.append('<table cellspacing="0">\n<thead>\n<tr>\n')
+ for i, field_name in enumerate(lookup_opts.admin.list_display):
+ try:
+ f = lookup_opts.get_field(field_name)
+ except meta.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__':
+ header = lookup_opts.verbose_name
+ else:
+ func = getattr(mod.Klass, field_name) # Let AttributeErrors propogate.
+ try:
+ header = func.short_description
+ except AttributeError:
+ header = func.__name__
+ # Non-field list_display values don't get ordering capability.
+ raw_template.append('<th>%s</th>' % meta.capfirst(header))
+ else:
+ if isinstance(f.rel, meta.ManyToOne) and f.null:
+ raw_template.append('<th>%s</th>' % meta.capfirst(f.verbose_name))
+ else:
+ th_classes = []
+ new_order_type = 'asc'
+ if field_name == order_field:
+ th_classes.append('sorted %sending' % order_type.lower())
+ new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type.lower()]
+ raw_template.append('<th%s><a href="%s">%s</a></th>' % \
+ ((th_classes and ' class="%s"' % ' '.join(th_classes) or ''),
+ get_query_string(params, {ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
+ meta.capfirst(f.verbose_name)))
+ raw_template.append('</tr>\n</thead>\n')
+ # Result rows.
+ pk = lookup_opts.pk.name
+ for i, result in enumerate(result_list):
+ raw_template.append('<tr class="row%s">\n' % (i % 2 + 1))
+ for j, field_name in enumerate(lookup_opts.admin.list_display):
+ row_class = ''
+ try:
+ f = lookup_opts.get_field(field_name)
+ except meta.FieldDoesNotExist:
+ # For non-field list_display values, the value is a method
+ # name. Execute the method.
+ try:
+ result_repr = strip_tags(str(getattr(result, field_name)()))
+ except ObjectDoesNotExist:
+ result_repr = EMPTY_CHANGELIST_VALUE
+ else:
+ field_val = getattr(result, f.name)
+ # Foreign-key fields are special: Use the repr of the
+ # related object.
+ if isinstance(f.rel, meta.ManyToOne):
+ if field_val is not None:
+ result_repr = getattr(result, 'get_%s' % f.rel.name)()
+ else:
+ result_repr = EMPTY_CHANGELIST_VALUE
+ # Dates are special: They're formatted in a certain way.
+ elif isinstance(f, meta.DateField):
+ if field_val:
+ if isinstance(f, meta.DateTimeField):
+ result_repr = dateformat.format(field_val, 'N j, Y, P')
+ else:
+ result_repr = dateformat.format(field_val, 'N j, Y')
+ else:
+ 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):
+ BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
+ result_repr = '<img src="/m/img/admin/icon-%s.gif" alt="%s" />' % (BOOLEAN_MAPPING[field_val], field_val)
+ # ImageFields are special: Use a thumbnail.
+ elif isinstance(f, meta.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):
+ if field_val is not None:
+ result_repr = ('%%.%sf' % f.decimal_places) % field_val
+ else:
+ result_repr = EMPTY_CHANGELIST_VALUE
+ # Fields with choices are special: Use the representation
+ # of the choice.
+ elif f.choices:
+ result_repr = dict(f.choices).get(field_val, EMPTY_CHANGELIST_VALUE)
+ else:
+ result_repr = strip_tags(str(field_val))
+ # Some browsers don't like empty "<td></td>"s.
+ if result_repr == '':
+ result_repr = '&nbsp;'
+ if j == 0: # First column is a special case
+ result_id = getattr(result, pk)
+ raw_template.append('<th%s><a href="%s/"%s>%s</a></th>' % \
+ (row_class, result_id, (is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), result_repr))
+ else:
+ raw_template.append('<td%s>%s</td>' % (row_class, result_repr))
+ raw_template.append('</tr>\n')
+ del result_list # to free memory
+ raw_template.append('</table>\n')
+ else:
+ raw_template.append('<p>No %s matched your search criteria.</p>' % opts.verbose_name_plural)
+
+ # Pagination.
+ raw_template.append('<p class="paginator">')
+ if (show_all and can_show_all) or not multi_page:
+ pass
+ else:
+ raw_template.append('Page &rsaquo; ')
+ ON_EACH_SIDE = 3
+ ON_ENDS = 2
+ DOT = '.'
+ # If there are 10 or fewer pages, display links to every page.
+ # Otherwise, do some fancy
+ if p.pages <= 10:
+ page_range = range(p.pages)
+ else:
+ # Insert "smart" pagination links, so that there are always ON_ENDS
+ # links at either end of the list of pages, and there are always
+ # ON_EACH_SIDE links at either end of the "current page" link.
+ page_range = []
+ if page_num > (ON_EACH_SIDE + ON_ENDS):
+ page_range.extend(range(0, ON_EACH_SIDE - 1))
+ page_range.append(DOT)
+ page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1))
+ else:
+ page_range.extend(range(0, page_num + 1))
+ if page_num < (p.pages - ON_EACH_SIDE - ON_ENDS - 1):
+ page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1))
+ page_range.append(DOT)
+ page_range.extend(range(p.pages - ON_ENDS, p.pages))
+ else:
+ page_range.extend(range(page_num + 1, p.pages))
+ for i in page_range:
+ if i == DOT:
+ raw_template.append('... ')
+ elif i == page_num:
+ raw_template.append('<span class="this-page">%d</span> ' % (i+1))
+ else:
+ raw_template.append('<a href="%s"%s>%d</a> ' % \
+ (get_query_string(params, {PAGE_VAR: i}), (i == p.pages-1 and ' class="end"' or ''), i+1))
+ raw_template.append('%s %s' % (result_count, result_count == 1 and opts.verbose_name or opts.verbose_name_plural))
+ if can_show_all and not show_all and multi_page:
+ raw_template.append('&nbsp;&nbsp;<a href="%s" class="showall">Show all</a>' % \
+ get_query_string(params, {ALL_VAR: ''}))
+ raw_template.append('</p>')
+
+ raw_template.append('</div>\n</div>')
+ raw_template.append('{% endblock %}\n')
+ t = template_loader.get_template_from_string(''.join(raw_template))
+ c = Context(request, {
+ 'title': (is_popup and 'Select %s' % opts.verbose_name or 'Select %s to change' % opts.verbose_name),
+ 'is_popup': is_popup,
+ })
+ return HttpResponse(t.render(c))
+
+def _get_flattened_data(field, val):
+ """
+ Returns a dictionary mapping the field's manipulator field names to its
+ "flattened" string values for the admin view. "val" is an instance of the
+ field's value.
+ """
+ if isinstance(field, meta.DateTimeField):
+ date_field, time_field = field.get_manipulator_field_names('')
+ 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 '')}
+ elif isinstance(field, meta.DateField):
+ return {field.name: (val is not None and val.strftime("%Y-%m-%d") or '')}
+ elif isinstance(field, meta.TimeField):
+ return {field.name: (val is not None and val.strftime("%H:%M:%S") or '')}
+ else:
+ return {field.name: val}
+
+use_raw_id_admin = lambda field: isinstance(field.rel, meta.ManyToOne) and field.rel.raw_id_admin
+
+def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects):
+ t = ['<div class="submit-row">']
+ if change or show_delete:
+ t.append('{%% if perms.%s.%s %%}{%% if not is_popup %%}<p class="float-left"><a href="delete/" class="deletelink">Delete</a></p>{%% endif %%}{%% endif %%}' % \
+ (app_label, opts.get_delete_permission()))
+ if change and opts.admin.save_as:
+ t.append('{%% if not is_popup %%}<input type="submit" value="Save as new" name="_saveasnew" %s/>{%% endif %%}' % \
+ (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
+ if not opts.admin.save_as or add:
+ t.append('{%% if not is_popup %%}<input type="submit" value="Save and add another" name="_addanother" %s/>{%% endif %%}' % \
+ (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
+ t.append('<input type="submit" value="Save and continue editing" name="_continue" %s/>' % \
+ (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
+ t.append('<input type="submit" value="Save" class="default" %s/>' % \
+ (ordered_objects and change and 'onclick="submitOrderForm();"' or ''))
+ t.append('</div>\n')
+ return t
+
+def _get_template(opts, app_label, add=False, change=False, show_delete=False, form_url=''):
+ ordered_objects = opts.get_ordered_objects()[:]
+ auto_populated_fields = [f for f in opts.fields if f.prepopulate_from]
+ t = ['{% extends "base_site" %}\n']
+ t.append('{% block extrahead %}')
+
+ # Put in any necessary JavaScript imports.
+ javascript_imports = ['/m/js/core.js', '/m/js/admin/RelatedObjectLookups.js']
+ if 'collapse' in ' '.join([f[1].get('classes', '') for f in opts.admin.fields]):
+ javascript_imports.append('/m/js/admin/CollapsedFieldsets.js')
+ if auto_populated_fields:
+ javascript_imports.append('/m/js/urlify.js')
+ if opts.has_field_type(meta.DateTimeField) or opts.has_field_type(meta.TimeField) or opts.has_field_type(meta.DateField):
+ javascript_imports.extend(['/m/js/calendar.js', '/m/js/admin/DateTimeShortcuts.js'])
+ if ordered_objects:
+ javascript_imports.extend(['/m/js/getElementsBySelector.js', '/m/js/dom-drag.js', '/m/js/admin/ordering.js'])
+ if opts.admin.js:
+ javascript_imports.extend(opts.admin.js)
+ for _, options in opts.admin.fields:
+ try:
+ for field_list in options['fields']:
+ for f in field_list:
+ if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface:
+ javascript_imports.extend(['/m/js/SelectBox.js', '/m/js/SelectFilter2.js'])
+ raise StopIteration
+ except StopIteration:
+ break
+ for j in javascript_imports:
+ t.append('<script type="text/javascript" src="%s"></script>' % j)
+
+ t.append('{% endblock %}\n')
+ if ordered_objects:
+ coltype = 'colMS'
+ else:
+ coltype = 'colM'
+ t.append('{%% block coltype %%}%s{%% endblock %%}\n' % coltype)
+ t.append('{%% block bodyclass %%}%s-%s change-form{%% endblock %%}\n' % (app_label, opts.object_name.lower()))
+ breadcrumb_title = add and "Add %s" % opts.verbose_name or '{{ original|striptags|truncatewords:"18" }}'
+ t.append('{%% block breadcrumbs %%}{%% if not is_popup %%}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../">%s</a> &rsaquo; %s</div>{%% endif %%}{%% endblock %%}\n' % \
+ (meta.capfirst(opts.verbose_name_plural), breadcrumb_title))
+ t.append('{% block content %}<div id="content-main">\n')
+ if change:
+ t.append('{% if not is_popup %}')
+ t.append('<ul class="object-tools"><li><a href="history/" class="historylink">History</a></li>')
+ if hasattr(opts.get_model_module().Klass, 'get_absolute_url'):
+ t.append('<li><a href="/r/%s/{{ object_id }}/" class="viewsitelink">View on site</a></li>' % opts.get_content_type_id())
+ t.append('</ul>\n')
+ t.append('{% endif %}')
+ t.append('<form ')
+ if opts.has_field_type(meta.FileField):
+ t.append('enctype="multipart/form-data" ')
+ t.append('action="%s" method="post">\n' % form_url)
+ t.append('{% if is_popup %}<input type="hidden" name="_popup" value="1">{% endif %}')
+ if opts.admin.save_on_top:
+ t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects))
+ t.append('{% if form.error_dict %}<p class="errornote">Please correct the error{{ form.error_dict.items|pluralize }} below.</p>{% endif %}\n')
+ for fieldset_name, options in opts.admin.fields:
+ t.append('<fieldset class="module aligned %s">\n\n' % options.get('classes', ''))
+ if fieldset_name:
+ t.append('<h2>%s</h2>\n' % fieldset_name)
+ for field_list in options['fields']:
+ t.append(_get_admin_field(field_list, 'form.', False, add, change))
+ for f in field_list:
+ if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface:
+ t.append('<script type="text/javascript">addEvent(window, "load", function(e) { SelectFilter.init("id_%s", "%s", %s); });</script>\n' % (f.name, f.verbose_name, f.rel.filter_interface-1))
+ t.append('</fieldset>\n')
+ if ordered_objects and change:
+ t.append('<fieldset class="module"><h2>Ordering</h2>')
+ t.append('<div class="form-row{% if form.order_.errors %} error{% endif %} ">\n')
+ t.append('{% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %}')
+ t.append('<p><label for="id_order_">Order:</label> {{ form.order_ }}</p>\n')
+ t.append('</div></fieldset>\n')
+ for rel_obj, rel_field in opts.get_inline_related_objects():
+ var_name = rel_obj.object_name.lower()
+ field_list = [f for f in rel_obj.fields + rel_obj.many_to_many if f.editable and f != rel_field]
+ t.append('<fieldset class="module%s">\n' % ((rel_field.rel.edit_inline_type != meta.TABULAR) and ' aligned' or ''))
+ view_on_site = ''
+ if change and hasattr(rel_obj, 'get_absolute_url'):
+ view_on_site = '{%% if %s.original %%}<a href="/r/{{ %s.content_type_id }}/{{ %s.original.id }}/">View on site</a>{%% endif %%}' % (var_name, var_name, var_name)
+ if rel_field.rel.edit_inline_type == meta.TABULAR:
+ t.append('<h2>%s</h2>\n<table>\n' % meta.capfirst(rel_obj.verbose_name_plural))
+ t.append('<thead><tr>')
+ for f in field_list:
+ if isinstance(f, meta.AutoField):
+ continue
+ t.append('<th%s>%s</th>' % (f.blank and ' class="optional"' or '', meta.capfirst(f.verbose_name)))
+ t.append('</tr></thead>\n')
+ t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name))
+ if change:
+ for f in field_list:
+ if use_raw_id_admin(f):
+ t.append('{%% if %s.original %%}' % var_name)
+ t.append('<tr class="row-label {% cycle row1,row2 %}">')
+ t.append('<td colspan="%s"><strong>{{ %s.original }}</strong></td>' % (30, var_name))
+ t.append('</tr>{% endif %}\n')
+ break
+ t.append('{%% if %s %%}\n' % ' or '.join(['%s.%s.errors' % (var_name, f.name) for f in field_list]))
+ t.append('<tr class="errorlist"><td colspan="%s">%s</td></tr>\n{%% endif %%}\n' % \
+ (len(field_list), ''.join(['{{ %s.%s.html_error_list }}' % (var_name, f.name) for f in field_list])))
+ t.append('<tr class="{% cycle row1,row2 %}">\n')
+ hidden_fields = []
+ for f in field_list:
+ form_widget = _get_admin_field_form_widget(f, var_name+'.', True, add, change)
+ # Don't put AutoFields within a <td>, because they're hidden.
+ if not isinstance(f, meta.AutoField):
+ # Fields with raw_id_admin=True get class="nowrap".
+ if use_raw_id_admin(f):
+ t.append('<td class="nowrap {%% if %s.%s.errors %%}error"{%% endif %%}">%s</td>\n' % (var_name, f.name, form_widget))
+ else:
+ t.append('<td{%% if %s.%s.errors %%} class="error"{%% endif %%}>%s</td>\n' % (var_name, f.name, form_widget))
+ else:
+ hidden_fields.append(form_widget)
+ if hasattr(rel_obj, 'get_absolute_url'):
+ t.append('<td>%s</td>\n' % view_on_site)
+ t.append('</tr>\n')
+ t.append('{% endfor %}\n</table>\n')
+ # Write out the hidden fields. We didn't write them out earlier
+ # because it would've been invalid HTML.
+ t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name))
+ t.extend(hidden_fields)
+ t.append('{% endfor %}\n')
+ else: # edit_inline_type == STACKED
+ t.append('{%% for %s in form.%s %%}' % (var_name, rel_obj.module_name))
+ t.append('<h2>%s #{{ forloop.counter }}</h2>' % meta.capfirst(rel_obj.verbose_name))
+ if view_on_site:
+ t.append('<p>%s</p>' % view_on_site)
+ for f in field_list:
+ # Don't put AutoFields within the widget -- just use the field.
+ if isinstance(f, meta.AutoField):
+ t.append(_get_admin_field_form_widget(f, var_name+'.', True, add, change))
+ else:
+ t.append(_get_admin_field([f], var_name+'.', True, add, change))
+ t.append('{% endfor %}\n')
+ t.append('</fieldset>\n')
+ t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects))
+ if add:
+ # Add focus to the first field on the form, if this is an "add" form.
+ t.append('<script type="text/javascript">document.getElementById("id_%s").focus();</script>' % \
+ opts.admin.fields[0][1]['fields'][0][0].get_manipulator_field_names('')[0])
+ if auto_populated_fields:
+ t.append('<script type="text/javascript">')
+ for field in auto_populated_fields:
+ if change:
+ t.append('document.getElementById("id_%s")._changed = true;' % field.name)
+ else:
+ t.append('document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name)
+ for f in field.prepopulate_from:
+ t.append('document.getElementById("id_%s").onkeyup = function() { var e = document.getElementById("id_%s"); if (!e._changed) { e.value = URLify(%s, %s);}};' % \
+ (f, field.name, ' + " " + '.join(['document.getElementById("id_%s").value' % g for g in field.prepopulate_from]), field.maxlength))
+ t.append('</script>\n')
+ if change and ordered_objects:
+ t.append('{% if form.order_objects %}<ul id="orderthese">{% for object in form.order_objects %}')
+ t.append('<li id="p{%% firstof %(x)s %%}"><span id="handlep{%% firstof %(x)s %%}">{{ object|truncatewords:"5" }}</span></li>' % \
+ {'x': ' '.join(['object.%s' % o.pk.name for o in ordered_objects])})
+ t.append('{% endfor %}</ul>{% endif %}\n')
+ t.append('</form>\n</div>\n{% endblock %}')
+ return ''.join(t)
+
+def _get_admin_field(field_list, name_prefix, rel, add, change):
+ "Returns the template code for editing the given list of fields in the admin template."
+ field_names = []
+ for f in field_list:
+ field_names.extend(f.get_manipulator_field_names(name_prefix))
+ div_class_names = ['form-row', '{%% if %s %%} error{%% endif %%}' % ' or '.join(['%s.errors' % n for n in field_names])]
+ # Assumes BooleanFields won't be stacked next to each other!
+ if isinstance(field_list[0], meta.BooleanField):
+ div_class_names.append('checkbox-row')
+ t = []
+ t.append('<div class="%s">\n' % ' '.join(div_class_names))
+ for n in field_names:
+ t.append('{%% if %s.errors %%}{{ %s.html_error_list }}{%% endif %%}\n' % (n, n))
+ for i, field in enumerate(field_list):
+ label_name = 'id_%s%s' % ((rel and "%s{{ forloop.counter0 }}." % name_prefix or ""), field.get_manipulator_field_names('')[0])
+ # BooleanFields are a special case, because the checkbox widget appears to
+ # the *left* of the label.
+ if isinstance(field, meta.BooleanField):
+ t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change))
+ t.append(' <label for="%s" class="vCheckboxLabel">%s</label>' % (label_name, meta.capfirst(field.verbose_name)))
+ else:
+ class_names = []
+ if not field.blank:
+ class_names.append('required')
+ if i > 0:
+ class_names.append('inline')
+ t.append('<label for="%s"%s>%s:</label> ' % (label_name, class_names and ' class="%s"' % ' '.join(class_names) or '', meta.capfirst(field.verbose_name)))
+ t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change))
+ if change and use_raw_id_admin(field):
+ obj_repr = '%soriginal.get_%s|truncatewords:"14"' % (rel and name_prefix or '', field.rel.name)
+ t.append('{%% if %s %%}&nbsp;<strong>{{ %s }}</strong>{%% endif %%}' % (obj_repr, obj_repr))
+ if field.help_text:
+ t.append('<p class="help">%s</p>\n' % field.help_text)
+ t.append('</div>\n\n')
+ return ''.join(t)
+
+def _get_admin_field_form_widget(field, name_prefix, rel, add, change):
+ "Returns JUST the formfield widget for the field's admin interface."
+ field_names = field.get_manipulator_field_names(name_prefix)
+ if isinstance(field, meta.DateTimeField):
+ return '<p class="datetime">Date: {{ %s }}<br />Time: {{ %s }}</p>' % tuple(field_names)
+ t = ['{{ %s }}' % n for n in field_names]
+ if change and isinstance(field, meta.FileField):
+ return '{%% if %soriginal.%s %%}Currently: <a href="{{ %soriginal.get_%s_url }}">{{ %soriginal.%s }}</a><br />Change: %s{%% else %%}%s{%% endif %%}' % \
+ (name_prefix, field.name, name_prefix, field.name, name_prefix, field.name, ''.join(t), ''.join(t))
+ field_id = 'id_%s%s' % ((rel and "%s{{ forloop.counter0 }}." % name_prefix or ""), field.get_manipulator_field_names('')[0])
+ # raw_id_admin fields get the little lookup link next to them
+ if use_raw_id_admin(field):
+ t.append(' <a href="../../../%s/%s/" class="related-lookup" id="lookup_%s" onclick="return showRelatedObjectLookupPopup(this);">' % \
+ (field.rel.to.app_label, field.rel.to.module_name, field_id))
+ t.append('<img src="/m/img/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>')
+ # fields with relationships to editable objects get an "add another" link,
+ # but only if the field doesn't have raw_admin ('cause in that case they get
+ # the "add" button in the popup)
+ elif field.rel and isinstance(field.rel, meta.ManyToOne) and field.rel.to.admin:
+ t.append('{%% if perms.%s.%s %%}' % (field.rel.to.app_label, field.rel.to.get_add_permission()))
+ t.append(' <a href="../../../%s/%s/add/" class="add-another" id="add_%s" onclick="return showAddAnotherPopup(this);">' % \
+ (field.rel.to.app_label, field.rel.to.module_name, field_id))
+ t.append('<img src="/m/img/admin/icon_addlink.gif" width="10" height="10" alt="Add Another" /></a>')
+ t.append('{% endif %}')
+ return ''.join(t)
+
+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 request.POST:
+ new_data = request.POST.copy()
+ if opts.has_field_type(meta.FileField):
+ new_data.update(request.FILES)
+ errors = manipulator.get_validation_errors(new_data)
+ if not errors and not request.POST.has_key("_preview"):
+ manipulator.do_html2python(new_data)
+ new_object = manipulator.save(new_data)
+ log.log_action(request.user.id, opts.get_content_type_id(), getattr(new_object, opts.pk.name), repr(new_object), log.ADDITION)
+ msg = 'The %s "%s" was added successfully.' % (opts.verbose_name, 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("%s You may edit it again below." % msg)
+ if request.POST.has_key("_popup"):
+ post_url_continue += "?_popup=1"
+ return HttpResponseRedirect(post_url_continue % new_object.id)
+ if request.POST.has_key("_popup"):
+ return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, %s, "%s");</script>' % \
+ (getattr(new_object, opts.pk.name), repr(new_object).replace('"', '\\"')))
+ elif request.POST.has_key("_addanother"):
+ request.user.add_message("%s You may add another %s below." % (msg, opts.verbose_name))
+ return HttpResponseRedirect(request.path)
+ else:
+ request.user.add_message(msg)
+ return HttpResponseRedirect(post_url)
+ if request.POST.has_key("_preview"):
+ manipulator.do_html2python(new_data)
+ else:
+ new_data = {}
+ # Add default data.
+ for f in opts.fields:
+ if f.has_default():
+ new_data.update(_get_flattened_data(f, f.get_default()))
+ # In required many-to-one fields with only one available choice,
+ # select that one available choice. Note: We have to check that
+ # the length of choices is *2*, not 1, because SelectFields always
+ # have an initial "blank" value.
+ elif not f.blank and ((isinstance(f.rel, meta.ManyToOne) and not f.rel.raw_id_admin) or f.choices) and len(manipulator[f.name].choices) == 2:
+ new_data[f.name] = manipulator[f.name].choices[1][0]
+ # In required many-to-many fields with only one available choice,
+ # select that one available choice.
+ for f in opts.many_to_many:
+ if not f.blank and not f.rel.edit_inline and len(manipulator[f.name].choices) == 1:
+ new_data[f.name] = [manipulator[f.name].choices[0][0]]
+ # Add default data for related objects.
+ for rel_opts, rel_field in opts.get_inline_related_objects():
+ var_name = rel_opts.object_name.lower()
+ for i in range(rel_field.rel.num_in_admin):
+ for f in rel_opts.fields + rel_opts.many_to_many:
+ if f.has_default():
+ for field_name in f.get_manipulator_field_names(''):
+ new_data['%s.%d.%s' % (var_name, i, field_name)] = f.get_default()
+ # Override the defaults with request.GET, if it exists.
+ new_data.update(request.GET)
+ errors = {}
+
+ # Populate the FormWrapper.
+ form = formfields.FormWrapper(manipulator, new_data, errors)
+ for rel_opts, rel_field in opts.get_inline_related_objects():
+ var_name = rel_opts.object_name.lower()
+ wrapper = []
+ for i in range(rel_field.rel.num_in_admin):
+ collection = {}
+ for f in rel_opts.fields + rel_opts.many_to_many:
+ if f.editable and f != rel_field and not isinstance(f, meta.AutoField):
+ for field_name in f.get_manipulator_field_names(''):
+ full_field_name = '%s.%d.%s' % (var_name, i, field_name)
+ collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, ''), errors.get(full_field_name, []))
+ wrapper.append(formfields.FormFieldCollection(collection))
+ setattr(form, rel_opts.module_name, wrapper)
+
+ c = Context(request, {
+ 'title': 'Add %s' % opts.verbose_name,
+ "form": form,
+ "is_popup": request.REQUEST.has_key("_popup"),
+ })
+ if object_id_override is not None:
+ c['object_id'] = object_id_override
+ raw_template = _get_template(opts, app_label, add=True, show_delete=show_delete, form_url=form_url)
+# return HttpResponse(raw_template, mimetype='text/plain')
+ t = template_loader.get_template_from_string(raw_template)
+ return HttpResponse(t.render(c))
+
+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/')
+ try:
+ manipulator = mod.ChangeManipulator(object_id)
+ except ObjectDoesNotExist:
+ raise Http404
+ inline_related_objects = opts.get_inline_related_objects()
+ if request.POST:
+ new_data = request.POST.copy()
+ if opts.has_field_type(meta.FileField):
+ new_data.update(request.FILES)
+ errors = manipulator.get_validation_errors(new_data)
+ if not errors and not request.POST.has_key("_preview"):
+ manipulator.do_html2python(new_data)
+ new_object = manipulator.save(new_data)
+
+ # 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(request.user.id, opts.get_content_type_id(), getattr(new_object, opts.pk.name), repr(new_object), log.CHANGE, change_message)
+ msg = 'The %s "%s" was changed successfully.' % (opts.verbose_name, new_object)
+ if request.POST.has_key("_continue"):
+ request.user.add_message("%s You may edit it again below." % msg)
+ 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 %s "%s" was added successfully. You may edit it again below.' % (opts.verbose_name, new_object))
+ return HttpResponseRedirect("../%s/" % new_object.id)
+ elif request.POST.has_key("_addanother"):
+ request.user.add_message("%s You may add another %s below." % (msg, opts.verbose_name))
+ return HttpResponseRedirect("../add/")
+ else:
+ request.user.add_message(msg)
+ return HttpResponseRedirect("../")
+ if request.POST.has_key("_preview"):
+ manipulator.do_html2python(new_data)
+ else:
+ # Populate new_data with a "flattened" version of the current data.
+ new_data = {}
+ obj = manipulator.original_object
+ for f in opts.fields:
+ new_data.update(_get_flattened_data(f, getattr(obj, f.name)))
+ for f in opts.many_to_many:
+ if not f.rel.edit_inline:
+ new_data[f.name] = [i.id for i in getattr(obj, 'get_%s' % f.name)()]
+ for rel_obj, rel_field in inline_related_objects:
+ var_name = rel_obj.object_name.lower()
+ for i, rel_instance in enumerate(getattr(obj, 'get_%s_list' % opts.get_rel_object_method_name(rel_obj, rel_field))()):
+ for f in rel_obj.fields:
+ if f.editable and f != rel_field:
+ for k, v in _get_flattened_data(f, getattr(rel_instance, f.name)).items():
+ new_data['%s.%d.%s' % (var_name, i, k)] = v
+ for f in rel_obj.many_to_many:
+ new_data['%s.%d.%s' % (var_name, i, f.name)] = [j.id for j in getattr(rel_instance, 'get_%s' % f.name)()]
+
+ # If the object has ordered objects on its admin page, get the existing
+ # order and flatten it into a comma-separated list of IDs.
+ id_order_list = []
+ for rel_obj in opts.get_ordered_objects():
+ id_order_list.extend(getattr(obj, 'get_%s_order' % rel_obj.object_name.lower())())
+ if id_order_list:
+ new_data['order_'] = ','.join(map(str, id_order_list))
+ errors = {}
+
+ # Populate the FormWrapper.
+ form = formfields.FormWrapper(manipulator, new_data, errors)
+ form.original = manipulator.original_object
+ form.order_objects = []
+ for rel_opts, rel_field in inline_related_objects:
+ var_name = rel_opts.object_name.lower()
+ wrapper = []
+ orig_list = getattr(manipulator.original_object, 'get_%s_list' % opts.get_rel_object_method_name(rel_opts, rel_field))()
+ count = len(orig_list) + rel_field.rel.num_extra_on_change
+ if rel_field.rel.min_num_in_admin:
+ count = max(count, rel_field.rel.min_num_in_admin)
+ if rel_field.rel.max_num_in_admin:
+ count = min(count, rel_field.rel.max_num_in_admin)
+ for i in range(count):
+ collection = {'original': (i < len(orig_list) and orig_list[i] or None)}
+ for f in rel_opts.fields + rel_opts.many_to_many:
+ if f.editable and f != rel_field:
+ for field_name in f.get_manipulator_field_names(''):
+ full_field_name = '%s.%d.%s' % (var_name, i, field_name)
+ collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, ''), errors.get(full_field_name, []))
+ wrapper.append(formfields.FormFieldCollection(collection))
+ setattr(form, rel_opts.module_name, wrapper)
+ if rel_opts.order_with_respect_to and rel_opts.order_with_respect_to.rel and rel_opts.order_with_respect_to.rel.to == opts:
+ form.order_objects.extend(orig_list)
+
+ c = Context(request, {
+ 'title': 'Change %s' % opts.verbose_name,
+ "form": form,
+ 'object_id': object_id,
+ 'original': manipulator.original_object,
+ 'is_popup' : request.REQUEST.has_key('_popup'),
+ })
+ raw_template = _get_template(opts, app_label, change=True)
+# return HttpResponse(raw_template, mimetype='text/plain')
+ t = template_loader.get_template_from_string(raw_template)
+ return HttpResponse(t.render(c))
+
+def _nest_help(obj, depth, val):
+ current = obj
+ for i in range(depth):
+ current = current[-1]
+ current.append(val)
+
+def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth):
+ "Helper function that recursively populates deleted_objects."
+ nh = _nest_help # Bind to local variable for performance
+ if current_depth > 16:
+ return # Avoid recursing too deep.
+ objects_seen = []
+ for rel_opts, rel_field in opts.get_all_related_objects():
+ if rel_opts in objects_seen:
+ continue
+ objects_seen.append(rel_opts)
+ rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field)
+ if isinstance(rel_field.rel, meta.OneToOne):
+ try:
+ sub_obj = getattr(obj, 'get_%s' % rel_opts_name)()
+ except ObjectDoesNotExist:
+ pass
+ else:
+ if rel_opts.admin:
+ p = '%s.%s' % (rel_opts.app_label, rel_opts.get_delete_permission())
+ if not user.has_perm(p):
+ perms_needed.add(rel_opts.verbose_name)
+ # We don't care about populating deleted_objects now.
+ continue
+ if rel_field.rel.edit_inline or not rel_opts.admin:
+ # Don't display link to edit, because it either has no
+ # admin or is edited inline.
+ nh(deleted_objects, current_depth, ['%s: %r' % (meta.capfirst(rel_opts.verbose_name), sub_obj), []])
+ else:
+ # Display a link to the admin page.
+ nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%r</a>' % \
+ (meta.capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name,
+ getattr(sub_obj, rel_opts.pk.name), sub_obj), []])
+ _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2)
+ else:
+ has_related_objs = False
+ for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)():
+ has_related_objs = True
+ if rel_field.rel.edit_inline or not rel_opts.admin:
+ # Don't display link to edit, because it either has no
+ # admin or is edited inline.
+ nh(deleted_objects, current_depth, ['%s: %s' % (meta.capfirst(rel_opts.verbose_name), strip_tags(repr(sub_obj))), []])
+ else:
+ # Display a link to the admin page.
+ nh(deleted_objects, current_depth, ['%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
+ (meta.capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []])
+ _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_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.
+ if rel_opts.admin and has_related_objs:
+ p = '%s.%s' % (rel_opts.app_label, rel_opts.get_delete_permission())
+ if not user.has_perm(p):
+ perms_needed.add(rel_opts.verbose_name)
+ for rel_opts, rel_field in opts.get_all_related_many_to_many_objects():
+ if rel_opts in objects_seen:
+ continue
+ objects_seen.append(rel_opts)
+ rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field)
+ has_related_objs = False
+ for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)():
+ has_related_objs = True
+ if rel_field.rel.edit_inline or not rel_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 %s in %s: %s' % \
+ (rel_field.name, rel_opts.verbose_name, strip_tags(repr(sub_obj))), []])
+ else:
+ # Display a link to the admin page.
+ nh(deleted_objects, current_depth, ['One or more %s in %s: <a href="../../../../%s/%s/%s/">%s</a>' % \
+ (rel_field.name, rel_opts.verbose_name, rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(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 rel_opts.admin and has_related_objs:
+ p = '%s.%s' % (rel_opts.app_label, rel_opts.get_change_permission())
+ if not user.has_perm(p):
+ perms_needed.add(rel_opts.verbose_name)
+
+def delete_stage(request, app_label, module_name, object_id):
+ import sets
+ mod, opts = _get_mod_opts(app_label, module_name)
+ if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()):
+ raise PermissionDenied
+ try:
+ obj = mod.get_object(**{'%s__exact' % opts.pk.name: object_id})
+ except ObjectDoesNotExist:
+ raise Http404
+
+ # Populate deleted_objects, a data structure of all related objects that
+ # will also be deleted.
+ deleted_objects = ['%s: <a href="../../%s/">%r</a>' % (meta.capfirst(opts.verbose_name), object_id, obj), []]
+ perms_needed = sets.Set()
+ _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)
+
+ if request.POST: # The user has already confirmed the deletion.
+ if perms_needed:
+ raise PermissionDenied
+ obj.delete()
+ obj_repr = repr(obj)
+ log.log_action(request.user.id, opts.get_content_type_id(), object_id, obj_repr, log.DELETION)
+ request.user.add_message('The %s "%s" was deleted successfully.' % (opts.verbose_name, obj_repr))
+ return HttpResponseRedirect("../../")
+ t = template_loader.get_template("delete_confirmation_generic")
+ c = Context(request, {
+ "title": "Are you sure?",
+ "object_name": opts.verbose_name,
+ "object": obj,
+ "deleted_objects": deleted_objects,
+ "perms_lacking": perms_needed,
+ })
+ return HttpResponse(t.render(c))
+
+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", "ASC"),), select_related=True)
+ # If no history was found, see whether this object even exists.
+ try:
+ obj = mod.get_object(id__exact=object_id)
+ except ObjectDoesNotExist:
+ raise Http404
+ t = template_loader.get_template('admin_object_history')
+ c = Context(request, {
+ 'title': 'Change history: %r' % obj,
+ 'action_list': action_list,
+ 'module_name': meta.capfirst(opts.verbose_name_plural),
+ 'object': obj,
+ })
+ return HttpResponse(t.render(c))
diff --git a/django/views/admin/template.py b/django/views/admin/template.py
new file mode 100644
index 0000000000..7c5e41e237
--- /dev/null
+++ b/django/views/admin/template.py
@@ -0,0 +1,70 @@
+from django.core import formfields, template_loader, validators
+from django.core import template
+from django.core.extensions import CMSContext as Context
+from django.utils.httpwrappers import HttpResponse
+from django.models.core import sites
+from django.conf import settings
+
+def template_validator(request):
+ """
+ Displays the template validator form, which finds and displays template
+ syntax errors.
+ """
+ # get a dict of {site_id : settings_module} for the validator
+ settings_modules = {}
+ for mod in settings.ADMIN_FOR:
+ settings_module = __import__(mod, '', '', [''])
+ settings_modules[settings_module.SITE_ID] = settings_module
+ manipulator = TemplateValidator(settings_modules)
+ new_data, errors = {}, {}
+ if request.POST:
+ new_data = request.POST.copy()
+ errors = manipulator.get_validation_errors(new_data)
+ if not errors:
+ request.user.add_message('The template is valid.')
+ t = template_loader.get_template('template_validator')
+ c = Context(request, {
+ 'title': 'Template validator',
+ 'form': formfields.FormWrapper(manipulator, new_data, errors),
+ })
+ return HttpResponse(t.render(c))
+
+class TemplateValidator(formfields.Manipulator):
+ def __init__(self, settings_modules):
+ self.settings_modules = settings_modules
+ site_list = sites.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]),
+ )
+
+ def isValidTemplate(self, field_data, all_data):
+ # get the settings module
+ # if the site isn't set, we don't raise an error since the site field will
+ try:
+ site_id = int(all_data.get('site', None))
+ except (ValueError, TypeError):
+ return
+ settings_module = self.settings_modules.get(site_id, None)
+ if settings_module is None:
+ return
+
+ # so that inheritance works in the site's context, register a new function
+ # for "extends" that uses the site's TEMPLATE_DIR instead
+ def new_do_extends(parser, token):
+ node = template_loader.do_extends(parser, token)
+ node.template_dirs = settings_module.TEMPLATE_DIRS
+ return node
+ template.register_tag('extends', new_do_extends)
+
+ # now validate the template using the new template dirs
+ # making sure to reset the extends function in any case
+ error = None
+ try:
+ tmpl = template_loader.get_template_from_string(field_data)
+ tmpl.render(template.Context({}))
+ except template.TemplateSyntaxError, e:
+ error = e
+ template.register_tag('extends', template_loader.do_extends)
+ if error:
+ raise validators.ValidationError, e.args
diff --git a/django/views/auth/__init__.py b/django/views/auth/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/auth/__init__.py
diff --git a/django/views/auth/login.py b/django/views/auth/login.py
new file mode 100644
index 0000000000..97988d0444
--- /dev/null
+++ b/django/views/auth/login.py
@@ -0,0 +1,62 @@
+from django.parts.auth.formfields import AuthenticationForm
+from django.core import formfields, template_loader
+from django.core.extensions import CMSContext as Context
+from django.models.auth import sessions
+from django.models.core import sites
+from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
+
+REDIRECT_FIELD_NAME = 'next'
+
+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/'
+ response = HttpResponseRedirect(redirect_to)
+ sessions.start_web_session(manipulator.get_user_id(), request, response)
+ return response
+ else:
+ errors = {}
+ response = HttpResponse()
+ # Set this cookie as a test to see whether the user accepts cookies
+ response.set_cookie(sessions.TEST_COOKIE_NAME, sessions.TEST_COOKIE_VALUE)
+ t = template_loader.get_template('registration/login')
+ c = Context(request, {
+ 'form': formfields.FormWrapper(manipulator, request.POST, errors),
+ REDIRECT_FIELD_NAME: redirect_to,
+ 'site_name': sites.get_current().name,
+ })
+ response.write(t.render(c))
+ return response
+
+def logout(request):
+ "Logs out the user and displays 'You are logged you' message."
+ if request.session:
+ # Do a redirect to this page until the session has been cleared.
+ response = HttpResponseRedirect(request.path)
+ # Delete the cookie by setting a cookie with an empty value and max_age=0
+ response.set_cookie(request.session.get_cookie()[0], '', max_age=0)
+ request.session.delete()
+ return response
+ else:
+ t = template_loader.get_template('registration/logged_out')
+ c = Context(request)
+ return HttpResponse(t.render(c))
+
+def logout_then_login(request):
+ "Logs out the user if he is logged in. Then redirects to the log-in page."
+ response = HttpResponseRedirect('/accounts/login/')
+ if request.session:
+ # Delete the cookie by setting a cookie with an empty value and max_age=0
+ response.set_cookie(request.session.get_cookie()[0], '', max_age=0)
+ request.session.delete()
+ return response
+
+def redirect_to_login(next):
+ "Redirects the user to the login page, passing the given 'next' page"
+ return HttpResponseRedirect('/accounts/login/?%s=%s' % (REDIRECT_FIELD_NAME, next))
diff --git a/django/views/comments/__init__.py b/django/views/comments/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/comments/__init__.py
diff --git a/django/views/comments/comments.py b/django/views/comments/comments.py
new file mode 100644
index 0000000000..213faf42e6
--- /dev/null
+++ b/django/views/comments/comments.py
@@ -0,0 +1,347 @@
+from django.core import formfields, template_loader, validators
+from django.core.mail import mail_admins, mail_managers
+from django.core.exceptions import Http404, ObjectDoesNotExist
+from django.core.extensions import CMSContext as Context
+from django.models.auth import sessions
+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 HttpResponse, 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
+import base64, datetime
+
+COMMENTS_PER_PAGE = 20
+
+class PublicCommentManipulator(AuthenticationForm):
+ "Manipulator that handles public registered comments"
+ def __init__(self, user, ratings_required, ratings_range, num_rating_choices):
+ AuthenticationForm.__init__(self)
+ self.ratings_range, self.num_rating_choices = ratings_range, num_rating_choices
+ choices = [(c, c) for c in ratings_range]
+ def get_validator_list(rating_num):
+ if rating_num <= num_rating_choices:
+ return [validators.RequiredIfOtherFieldsGiven(['rating%d' % i for i in range(1, 9) if i != rating_num], "This rating is required because you've entered at least one other rating.")]
+ else:
+ return []
+ self.fields.extend([
+ formfields.LargeTextField(field_name="comment", maxlength=3000, is_required=True,
+ validator_list=[self.hasNoProfanities]),
+ formfields.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,
+ is_required=ratings_required and num_rating_choices > 1,
+ validator_list=get_validator_list(2),
+ ),
+ formfields.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,
+ is_required=ratings_required and num_rating_choices > 3,
+ validator_list=get_validator_list(4),
+ ),
+ formfields.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,
+ is_required=ratings_required and num_rating_choices > 5,
+ validator_list=get_validator_list(6),
+ ),
+ formfields.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,
+ is_required=ratings_required and num_rating_choices > 7,
+ validator_list=get_validator_list(8),
+ ),
+ ])
+ if not user.is_anonymous():
+ self["username"].is_required = False
+ self["username"].validator_list = []
+ self["password"].is_required = False
+ self["password"].validator_list = []
+ self.user_cache = user
+
+ def hasNoProfanities(self, field_data, all_data):
+ if 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"],
+ 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)
+
+ 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"],
+ 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
+ # the comment was posted successfully.
+ if old.submit_date.date() == today and old.comment == c.comment \
+ and old.rating1 == c.rating1 and old.rating2 == c.rating2 \
+ and old.rating3 == c.rating3 and old.rating4 == c.rating4 \
+ and old.rating5 == c.rating5 and old.rating6 == c.rating6 \
+ and old.rating7 == c.rating7 and old.rating8 == c.rating8:
+ return old
+ # If the user is leaving a rating, invalidate all old ratings.
+ if c.rating1 is not None:
+ old.valid_rating = False
+ old.save()
+ 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:
+ message = 'This comment was posted by a user who has posted fewer than %s comments:\n\n%s' % \
+ (COMMENTS_FIRST_FEW, 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_groups()]:
+ message = 'This comment was posted by a sketchy user:\n\n%s' % 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):
+ "Manipulator that handles public free (unregistered) comments"
+ def __init__(self):
+ self.fields = (
+ formfields.TextField(field_name="person_name", maxlength=50, is_required=True,
+ validator_list=[self.hasNoProfanities]),
+ formfields.LargeTextField(field_name="comment", maxlength=3000, is_required=True,
+ validator_list=[self.hasNoProfanities]),
+ )
+
+ def hasNoProfanities(self, field_data, all_data):
+ if 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"],
+ 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)
+
+ def save(self, new_data):
+ today = datetime.date.today()
+ c = self.get_comment(new_data)
+ # 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"],
+ 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):
+ if old_comment.comment == c.comment:
+ return old_comment
+ c.save()
+ return c
+
+def post_comment(request):
+ """
+ Post a comment
+
+ Redirects to the `comments.comments.comment_was_posted` view upon success.
+
+ Templates: `comment_preview`
+ Context:
+ comment
+ the comment being posted
+ comment_form
+ the comment form
+ options
+ comment options
+ target
+ comment target
+ hash
+ security hash (must be included in a posted form to succesfully
+ post a comment).
+ rating_options
+ comment ratings options
+ ratings_optional
+ are ratings optional?
+ ratings_required
+ are ratings required?
+ rating_range
+ range of ratings
+ rating_choices
+ choice of ratings
+ """
+ if not request.POST:
+ raise Http404, "Only POSTs are allowed"
+ try:
+ 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"
+ 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:
+ 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))
+ else:
+ rating_range, rating_choices = [], []
+ content_type_id, object_id = target.split(':') # target is something like '52:5157'
+ try:
+ obj = contenttypes.get_object(id__exact=content_type_id).get_object_for_this_type(id__exact=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'
+ new_data = request.POST.copy()
+ 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
+ response = HttpResponse()
+ manipulator = PublicCommentManipulator(request.user,
+ ratings_required=comments.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']):
+ sessions.start_web_session(manipulator.get_user_id(), request, response)
+ if errors or request.POST.has_key('preview'):
+ class CommentFormWrapper(formfields.FormWrapper):
+ def __init__(self, manipulator, new_data, errors, rating_choices):
+ formfields.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))]
+ for i, f in enumerate(field_list):
+ f.choice = rating_choices[i]
+ return field_list
+ comment = errors and '' or manipulator.get_comment(new_data)
+ comment_form = CommentFormWrapper(manipulator, new_data, errors, rating_choices)
+ t = template_loader.get_template('comments/preview')
+ c = Context(request, {
+ '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,
+ 'rating_range': rating_range,
+ 'rating_choices': rating_choices,
+ })
+ 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:
+ mail_admins("Banned IP attempted to post comment", str(request.POST) + "\n\n" + str(request.META))
+ else:
+ manipulator.do_html2python(new_data)
+ comment = manipulator.save(new_data)
+ return HttpResponseRedirect("/comments/posted/?c=%s:%s" % (content_type_id, object_id))
+ else:
+ raise Http404, "The comment form didn't provide either 'preview' or 'post'"
+ response.write(t.render(c))
+ return response
+
+def post_free_comment(request):
+ """
+ Post a free comment (not requiring a log in)
+
+ Redirects to `comments.comments.comment_was_posted` view on success.
+
+ Templates: `comment_free_preview`
+ Context:
+ comment
+ comment being posted
+ comment_form
+ comment form object
+ options
+ comment options
+ target
+ comment target
+ hash
+ security hash (must be included in a posted form to succesfully
+ post a comment).
+ """
+ if not request.POST:
+ raise Http404, "Only POSTs are allowed"
+ try:
+ 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:
+ 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(id__exact=content_type_id)
+ try:
+ obj = content_type.get_object_for_this_type(id__exact=object_id)
+ except ObjectDoesNotExist:
+ raise Http404, "The comment form had an invalid 'target' parameter -- the object ID was invalid"
+ option_list = options.split(',')
+ new_data = request.POST.copy()
+ 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
+ response = HttpResponse()
+ 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)
+ t = template_loader.get_template('comments/free_preview')
+ c = Context(request, {
+ 'comment': comment,
+ 'comment_form': formfields.FormWrapper(manipulator, new_data, errors),
+ 'options': options,
+ 'target': target,
+ 'hash': security_hash,
+ })
+ 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:
+ from django.core.mail import mail_admins
+ mail_admins("Practical joker", str(request.POST) + "\n\n" + str(request.META))
+ else:
+ manipulator.do_html2python(new_data)
+ comment = manipulator.save(new_data)
+ return HttpResponseRedirect("/comments/posted/?c=%s:%s" % (content_type_id, object_id))
+ else:
+ raise Http404, "The comment form didn't provide either 'preview' or 'post'"
+ response.write(t.render(c))
+ return response
+
+def comment_was_posted(request):
+ """
+ Display "comment was posted" success page
+
+ Templates: `comment_posted`
+ Context:
+ object
+ The object the comment was posted on
+ """
+ obj = None
+ if request.GET.has_key('c'):
+ content_type_id, object_id = request.GET['c'].split(':')
+ try:
+ content_type = contenttypes.get_object(id__exact=content_type_id)
+ obj = content_type.get_object_for_this_type(id__exact=object_id)
+ except ObjectDoesNotExist:
+ pass
+ t = template_loader.get_template('comments/posted')
+ c = Context(request, {
+ 'object': obj,
+ })
+ return HttpResponse(t.render(c))
diff --git a/django/views/comments/karma.py b/django/views/comments/karma.py
new file mode 100644
index 0000000000..84f0ca852d
--- /dev/null
+++ b/django/views/comments/karma.py
@@ -0,0 +1,34 @@
+from django.core import template_loader
+from django.core.extensions import CMSContext as Context
+from django.core.exceptions import Http404
+from django.models.comments import comments, karma
+from django.utils.httpwrappers import HttpResponse
+
+def vote(request, comment_id, vote):
+ """
+ Rate a comment (+1 or -1)
+
+ Templates: `karma_vote_accepted`
+ Context:
+ comment
+ `comments.comments` object being rated
+ """
+ rating = {'up': 1, 'down': -1}.get(vote, False)
+ if not rating:
+ raise Http404, "Invalid vote"
+ if request.user.is_anonymous():
+ raise Http404, "Anonymous users cannot vote"
+ try:
+ comment = comments.get_object(id__exact=comment_id)
+ except comments.CommentDoesNotExist:
+ raise Http404, "Invalid comment ID"
+ if comment.user_id == request.user.id:
+ raise Http404, "No voting for yourself"
+ karma.vote(request.user.id, comment_id, rating)
+ # Reload comment to ensure we have up to date karma count
+ comment = comments.get_object(id__exact=comment_id)
+ t = template_loader.get_template('comments/karma_vote_accepted')
+ c = Context(request, {
+ 'comment': comment
+ })
+ return HttpResponse(t.render(c))
diff --git a/django/views/comments/userflags.py b/django/views/comments/userflags.py
new file mode 100644
index 0000000000..051ecb19fe
--- /dev/null
+++ b/django/views/comments/userflags.py
@@ -0,0 +1,82 @@
+from django.core import template_loader
+from django.core.extensions import CMSContext as Context
+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 HttpResponse, HttpResponseRedirect
+from django.conf.settings import SITE_ID
+
+def flag(request, comment_id):
+ """
+ Flags a comment. Confirmation on GET, action on POST.
+
+ Templates: `comments/flag_verify`, `comments/flag_done`
+ Context:
+ comment
+ the flagged `comments.comments` object
+ """
+ try:
+ comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+ except comments.CommentDoesNotExist:
+ raise Http404
+ if request.POST:
+ userflags.flag(comment, request.user)
+ return HttpResponseRedirect('%sdone/' % request.path)
+ t = template_loader.get_template('comments/flag_verify')
+ c = Context(request, {
+ 'comment': comment,
+ })
+ return HttpResponse(t.render(c))
+flag = login_required(flag)
+
+def flag_done(request, comment_id):
+ try:
+ comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+ except comments.CommentDoesNotExist:
+ raise Http404
+ t = template_loader.get_template('comments/flag_done')
+ c = Context(request, {
+ 'comment': comment,
+ })
+ return HttpResponse(t.render(c))
+
+def delete(request, comment_id):
+ """
+ Deletes a comment. Confirmation on GET, action on POST.
+
+ Templates: `comments/delete_verify`, `comments/delete_done`
+ Context:
+ comment
+ the flagged `comments.comments` object
+ """
+ try:
+ comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+ except comments.CommentDoesNotExist:
+ raise Http404
+ if not comments.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.save()
+ return HttpResponseRedirect('%sdone/' % request.path)
+ t = template_loader.get_template('comments/delete_verify')
+ c = Context(request, {
+ 'comment': comment,
+ })
+ return HttpResponse(t.render(c))
+delete = login_required(delete)
+
+def delete_done(request, comment_id):
+ try:
+ comment = comments.get_object(id__exact=comment_id, site_id__exact=SITE_ID)
+ except comments.CommentDoesNotExist:
+ raise Http404
+ t = template_loader.get_template('comments/delete_done')
+ c = Context(request, {
+ 'comment': comment,
+ })
+ return HttpResponse(t.render(c))
diff --git a/django/views/core/__init__.py b/django/views/core/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/core/__init__.py
diff --git a/django/views/core/flatfiles.py b/django/views/core/flatfiles.py
new file mode 100644
index 0000000000..68af68a355
--- /dev/null
+++ b/django/views/core/flatfiles.py
@@ -0,0 +1,34 @@
+from django.core import template_loader
+from django.core.exceptions import Http404
+from django.core.extensions import CMSContext as Context
+from django.models.core import flatfiles
+from django.utils.httpwrappers import HttpResponse
+from django.conf.settings import SITE_ID
+
+def flat_file(request, url):
+ """
+ Flat file view
+
+ Models: `core.flatfiles`
+ Templates: Uses the template defined by the ``template_name`` field,
+ or `flatfiles/default` if template_name is not defined.
+ Context:
+ flatfile
+ `flatfiles.flatfiles` object
+ """
+ if not url.startswith('/'):
+ url = "/" + url
+ try:
+ f = flatfiles.get_object(url__exact=url, sites__id__exact=SITE_ID)
+ except flatfiles.FlatFileDoesNotExist:
+ raise Http404
+ # If registration is required for accessing this page, and the user isn't
+ # logged in, redirect to the login page.
+ if request.user.is_anonymous() and f.registration_required:
+ from django.views.auth.login import redirect_to_login
+ return redirect_to_login(request.path)
+ t = template_loader.select_template([f.template_name, 'flatfiles/default'])
+ c = Context(request, {
+ 'flatfile': f,
+ })
+ return HttpResponse(t.render(c))
diff --git a/django/views/decorators/__init__.py b/django/views/decorators/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/decorators/__init__.py
diff --git a/django/views/decorators/auth.py b/django/views/decorators/auth.py
new file mode 100644
index 0000000000..ae27fe33a1
--- /dev/null
+++ b/django/views/decorators/auth.py
@@ -0,0 +1,12 @@
+def login_required(view_func):
+ """
+ Decorator for views that checks that the user is logged in, redirecting
+ to the log-in page if necessary.
+ """
+ from django.views.auth.login import redirect_to_login
+ def _checklogin(request, *args, **kwargs):
+ if request.user.is_anonymous():
+ return redirect_to_login(request.path)
+ else:
+ return view_func(request, *args, **kwargs)
+ return _checklogin
diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py
new file mode 100644
index 0000000000..52501f74d5
--- /dev/null
+++ b/django/views/decorators/cache.py
@@ -0,0 +1,64 @@
+from django.core.cache import cache
+from django.utils.httpwrappers import HttpResponseNotModified
+import cStringIO, datetime, gzip, md5
+
+# From http://www.xhaus.com/alan/python/httpcomp.html#gzip
+# Used with permission.
+def compress_string(s):
+ zbuf = cStringIO.StringIO()
+ zfile = gzip.GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
+ zfile.write(s)
+ zfile.close()
+ return zbuf.getvalue()
+
+def cache_page(view_func, cache_timeout, key_prefix=''):
+ """
+ Decorator for views that tries getting the page from the cache and
+ populates the cache if the page isn't in the cache yet. Also takes care
+ of ETags and gzips the page if the client supports it.
+
+ The cache is keyed off of the page's URL plus the optional key_prefix
+ variable. Use key_prefix if your Django setup has multiple sites that
+ use cache; otherwise the cache for one site would affect the other. A good
+ example of key_prefix is to use sites.get_current().domain, because that's
+ unique across all CMS instances on a particular server.
+ """
+ def _check_cache(request, *args, **kwargs):
+ try:
+ accept_encoding = request.META['HTTP_ACCEPT_ENCODING']
+ except KeyError:
+ accept_encoding = ''
+ accepts_gzip = 'gzip' in accept_encoding
+ cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, accepts_gzip)
+ response = cache.get(cache_key, None)
+ if response is None:
+ response = view_func(request, *args, **kwargs)
+ content = response.get_content_as_string('utf-8')
+ if accepts_gzip:
+ content = compress_string(content)
+ response.content = content
+ response['Content-Encoding'] = 'gzip'
+ response['ETag'] = md5.new(content).hexdigest()
+ response['Content-Length'] = '%d' % len(content)
+ response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
+ cache.set(cache_key, response, cache_timeout)
+ else:
+ # Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet
+ try:
+ if_none_match = request.META['HTTP_IF_NONE_MATCH']
+ except KeyError:
+ if_none_match = None
+ try:
+ if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE']
+ except KeyError:
+ if_modified_since = None
+ if if_none_match is None and if_modified_since is None:
+ pass
+ elif if_none_match is not None and response['ETag'] != if_none_match:
+ pass
+ elif if_modified_since is not None and response['Last-Modified'] != if_modified_since:
+ pass
+ else:
+ return HttpResponseNotModified()
+ return response
+ return _check_cache
diff --git a/django/views/defaults.py b/django/views/defaults.py
new file mode 100644
index 0000000000..72cb027b00
--- /dev/null
+++ b/django/views/defaults.py
@@ -0,0 +1,72 @@
+from django.core import template_loader
+from django.core.exceptions import Http404, ObjectDoesNotExist
+from django.core.extensions import CMSContext as Context
+from django.models.core import sites
+from django.utils import httpwrappers
+
+def shortcut(request, content_type_id, object_id):
+ from django.models.core import contenttypes
+ try:
+ content_type = contenttypes.get_object(id__exact=content_type_id)
+ obj = content_type.get_object_for_this_type(id__exact=object_id)
+ except ObjectDoesNotExist:
+ raise Http404, "Content type %s object %s doesn't exist" % (content_type_id, object_id)
+ if not hasattr(obj, 'get_absolute_url'):
+ raise Http404, "%s objects don't have get_absolute_url() methods" % content_type.name
+ object_domain = None
+ if hasattr(obj, 'get_sites'):
+ site_list = obj.get_sites()
+ if site_list:
+ object_domain = site_list[0].domain
+ elif hasattr(obj, 'get_site'):
+ try:
+ object_domain = obj.get_site().domain
+ except sites.SiteDoesNotExist:
+ pass
+ try:
+ object_domain = sites.get_current().domain
+ except sites.SiteDoesNotExist:
+ pass
+ if not object_domain:
+ return httpwrappers.HttpResponseRedirect(obj.get_absolute_url())
+ return httpwrappers.HttpResponseRedirect('http://%s%s' % (object_domain, obj.get_absolute_url()))
+
+def page_not_found(request):
+ """
+ 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
+ """
+ from django.models.core import redirects
+ from django.conf.settings import APPEND_SLASH, SITE_ID
+ path = request.get_full_path()
+ try:
+ r = redirects.get_object(site_id__exact=SITE_ID, old_path__exact=path)
+ except redirects.RedirectDoesNotExist:
+ r = None
+ if r is None and 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:
+ pass
+ if r is not None:
+ if r == '':
+ return httpwrappers.HttpResponseGone()
+ return httpwrappers.HttpResponseRedirect(r.new_path)
+ t = template_loader.get_template('404')
+ c = Context(request)
+ return httpwrappers.HttpResponseNotFound(t.render(c))
+
+def server_error(request):
+ """
+ 500 Error handler
+
+ Templates: `500`
+ Context: None
+ """
+ t = template_loader.get_template('500')
+ c = Context(request)
+ return httpwrappers.HttpResponseServerError(t.render(c))
diff --git a/django/views/generic/__init__.py b/django/views/generic/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/generic/__init__.py
diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py
new file mode 100644
index 0000000000..7db02813aa
--- /dev/null
+++ b/django/views/generic/date_based.py
@@ -0,0 +1,223 @@
+from django.core import template_loader
+from django.core.exceptions import Http404, ObjectDoesNotExist
+from django.core.extensions import CMSContext as Context
+from django.core.xheaders import populate_xheaders
+from django.models import get_module
+from django.utils.httpwrappers import HttpResponse
+import datetime, time
+
+def archive_index(request, app_label, module_name, date_field, num_latest=15, template_name=None, extra_lookup_kwargs={}, extra_context=None):
+ """
+ Generic top-level archive of date-based objects.
+
+ Templates: ``<app_label>/<module_name>_archive``
+ 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]
+ if not date_list:
+ raise Http404("No %s.%s available" % (app_label, module_name))
+
+ if num_latest:
+ lookup_kwargs.update({
+ 'limit': num_latest,
+ 'order_by': ((date_field, 'DESC'),),
+ })
+ latest = mod.get_list(**lookup_kwargs)
+ else:
+ latest = None
+
+ if not template_name:
+ template_name = "%s/%s_archive" % (app_label, module_name)
+ t = template_loader.get_template(template_name)
+ c = Context(request, {
+ 'date_list' : date_list,
+ 'latest' : latest,
+ })
+ if extra_context:
+ c.update(extra_context)
+ return HttpResponse(t.render(c))
+
+def archive_year(request, year, app_label, module_name, date_field, template_name=None, extra_lookup_kwargs={}, extra_context=None):
+ """
+ Generic yearly archive view.
+
+ Templates: ``<app_label>/<module_name>_archive_year``
+ Context:
+ date_list
+ List of months in this year with objects
+ year
+ This year
+ """
+ mod = get_module(app_label, module_name)
+ 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)
+ if not date_list:
+ raise Http404
+ if not template_name:
+ template_name = "%s/%s_archive_year" % (app_label, module_name)
+ t = template_loader.get_template(template_name)
+ c = Context(request, {
+ 'date_list': date_list,
+ 'year': year,
+ })
+ if extra_context:
+ c.update(extra_context)
+ return HttpResponse(t.render(c))
+
+def archive_month(request, year, month, app_label, module_name, date_field, template_name=None, extra_lookup_kwargs={}, extra_context=None):
+ """
+ Generic monthly archive view.
+
+ Templates: ``<app_label>/<module_name>_archive_month``
+ Context:
+ month:
+ this month
+ object_list:
+ list of objects published in the given month
+ """
+ try:
+ date = datetime.date(*time.strptime(year+month, '%Y%b')[:3])
+ except ValueError:
+ raise Http404
+ mod = get_module(app_label, module_name)
+ now = datetime.datetime.now()
+ # Calculate first and last day of month, for use in a date-range lookup.
+ first_day = date.replace(day=1)
+ last_day = date
+ for i in (31, 30, 29, 28):
+ try:
+ last_day = last_day.replace(day=i)
+ except ValueError:
+ continue
+ else:
+ break
+ 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 date >= now:
+ lookup_kwargs['%s__lte' % date_field] = now
+ lookup_kwargs.update(extra_lookup_kwargs)
+ object_list = mod.get_list(**lookup_kwargs)
+ if not object_list:
+ raise Http404
+ if not template_name:
+ template_name = "%s/%s_archive_month" % (app_label, module_name)
+ t = template_loader.get_template(template_name)
+ c = Context(request, {
+ 'object_list': object_list,
+ 'month': date,
+ })
+ if extra_context:
+ c.update(extra_context)
+ return HttpResponse(t.render(c))
+
+def archive_day(request, year, month, day, app_label, module_name, date_field, template_name=None, extra_lookup_kwargs={}, extra_context=None, allow_empty=False):
+ """
+ Generic daily archive view.
+
+ Templates: ``<app_label>/<module_name>_archive_day``
+ Context:
+ object_list:
+ list of objects published that day
+ day:
+ (datetime) the day
+ previous_day
+ (datetime) the previous day
+ next_day
+ (datetime) the next day, or None if the current day is today
+ """
+ try:
+ date = datetime.date(*time.strptime(year+month+day, '%Y%b%d')[:3])
+ except ValueError:
+ raise Http404
+ mod = get_module(app_label, module_name)
+ 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:
+ lookup_kwargs['%s__lte' % date_field] = now
+ lookup_kwargs.update(extra_lookup_kwargs)
+ object_list = mod.get_list(**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)
+ t = template_loader.get_template(template_name)
+ c = Context(request, {
+ 'object_list': object_list,
+ 'day': date,
+ 'previous_day': date - datetime.timedelta(days=1),
+ 'next_day': (date < datetime.date.today()) and (date + datetime.timedelta(days=1)) or None,
+ })
+ if extra_context:
+ c.update(extra_context)
+ return HttpResponse(t.render(c))
+
+def archive_today(request, **kwargs):
+ """
+ Generic daily archive view for today. Same as archive_day view.
+ """
+ today = datetime.date.today()
+ kwargs.update({
+ 'year': str(today.year),
+ 'month': today.strftime('%b').lower(),
+ 'day': str(today.day),
+ })
+ return archive_day(request, **kwargs)
+
+def object_detail(request, year, month, day, app_label, module_name, date_field, object_id=None, slug=None, slug_field=None, template_name=None, extra_lookup_kwargs={}, extra_context=None):
+ """
+ Generic detail view from year/month/day/slug or year/month/day/id structure.
+
+ Templates: ``<app_label>/<module_name>_detail``
+ Context:
+ object:
+ the object to be detailed
+ """
+ try:
+ date = datetime.date(*time.strptime(year+month+day, '%Y%b%d')[:3])
+ except ValueError:
+ raise Http404
+ mod = get_module(app_label, module_name)
+ 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:
+ lookup_kwargs['%s__lte' % date_field] = now
+ if object_id:
+ lookup_kwargs['%s__exact' % mod.Klass._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)
+ try:
+ object = mod.get_object(**lookup_kwargs)
+ except ObjectDoesNotExist:
+ raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs))
+ if not template_name:
+ template_name = "%s/%s_detail" % (app_label, module_name)
+ t = template_loader.get_template(template_name)
+ c = Context(request, {
+ 'object': object,
+ })
+ if extra_context:
+ c.update(extra_context)
+ response = HttpResponse(t.render(c))
+ populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name))
+ return response
diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
new file mode 100644
index 0000000000..8b3ae5b9e9
--- /dev/null
+++ b/django/views/generic/list_detail.py
@@ -0,0 +1,106 @@
+from django import models
+from django.core import template_loader
+from django.utils.httpwrappers import HttpResponse
+from django.core.xheaders import populate_xheaders
+from django.core.extensions import CMSContext as Context
+from django.core.paginator import ObjectPaginator, InvalidPage
+from django.core.exceptions import Http404, ObjectDoesNotExist
+
+def object_list(request, app_label, module_name, paginate_by=None, allow_empty=False, template_name=None, extra_lookup_kwargs={}, extra_context=None):
+ """
+ Generic list of objects.
+
+ Templates: ``<app_label>/<module_name>_list``
+ Context:
+ object_list
+ list of objects
+ is_paginated
+ are the results paginated?
+ results_per_page
+ number of objects per page (if paginated)
+ has_next
+ is there a next page?
+ has_previous
+ is there a prev page?
+ page
+ the current page
+ next
+ the next page
+ previous
+ the previous page
+ pages
+ number of pages, total
+ """
+ mod = models.get_module(app_label, module_name)
+ lookup_kwargs = extra_lookup_kwargs.copy()
+ if paginate_by:
+ paginator = ObjectPaginator(mod, lookup_kwargs, paginate_by)
+ page = request.GET.get('page', 0)
+ try:
+ object_list = paginator.get_page(page)
+ except InvalidPage:
+ raise Http404
+ page = int(page)
+ c = Context(request, {
+ 'object_list': object_list,
+ 'is_paginated' : True,
+ 'results_per_page' : paginate_by,
+ 'has_next': paginator.has_next_page(page),
+ 'has_previous': paginator.has_previous_page(page),
+ 'page': page + 1,
+ 'next': page + 1,
+ 'previous': page - 1,
+ 'pages': paginator.pages,
+ })
+ else:
+ object_list = mod.get_list(**lookup_kwargs)
+ c = Context(request, {
+ 'object_list' : object_list,
+ 'is_paginated' : False
+ })
+ if len(object_list) == 0 and not allow_empty:
+ raise Http404
+ if extra_context:
+ c.update(extra_context)
+ if not template_name:
+ template_name = "%s/%s_list" % (app_label, module_name)
+ 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, slug_field=None, template_name=None, template_name_field=None, extra_lookup_kwargs={}, extra_context=None):
+ """
+ Generic list of objects.
+
+ Templates: ``<app_label>/<module_name>_list``
+ Context:
+ object
+ the object (whoa!)
+ """
+ mod = models.get_module(app_label, module_name)
+ lookup_kwargs = {}
+ if object_id:
+ lookup_kwargs['%s__exact' % mod.Klass._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/slug_field")
+ lookup_kwargs.update(extra_lookup_kwargs)
+ try:
+ object = mod.get_object(**lookup_kwargs)
+ except ObjectDoesNotExist:
+ raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs))
+ if not template_name:
+ template_name = "%s/%s_detail" % (app_label, module_name)
+ if template_name_field:
+ template_name_list = [getattr(object, template_name_field), template_name]
+ t = template_loader.select_template(template_name_list)
+ else:
+ t = template_loader.get_template(template_name)
+ c = Context(request, {
+ 'object' : object,
+ })
+ if extra_context:
+ c.update(extra_context)
+ response = HttpResponse(t.render(c))
+ populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name))
+ return response
diff --git a/django/views/registration/__init__.py b/django/views/registration/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/registration/__init__.py
diff --git a/django/views/registration/passwords.py b/django/views/registration/passwords.py
new file mode 100644
index 0000000000..0c06341af2
--- /dev/null
+++ b/django/views/registration/passwords.py
@@ -0,0 +1,109 @@
+from django.core import formfields, template_loader, validators
+from django.core.extensions import CMSContext as Context
+from django.models.auth import users
+from django.views.decorators.auth import login_required
+from django.utils.httpwrappers import HttpResponse, 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 = template_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(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)
+ t = template_loader.get_template('registration/password_reset_form')
+ c = Context(request, {
+ 'form': formfields.FormWrapper(form, new_data, errors),
+ })
+ return HttpResponse(t.render(c))
+
+def password_reset_done(request):
+ t = template_loader.get_template('registration/password_reset_done')
+ c = Context(request, {})
+ return HttpResponse(t.render(c))
+
+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)
+ t = template_loader.get_template('registration/password_change_form')
+ c = Context(request, {
+ 'form': formfields.FormWrapper(form, new_data, errors),
+ })
+ return HttpResponse(t.render(c))
+password_change = login_required(password_change)
+
+def password_change_done(request):
+ t = template_loader.get_template('registration/password_change_done')
+ c = Context(request, {})
+ return HttpResponse(t.render(c))
diff --git a/django/views/rss/__init__.py b/django/views/rss/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/views/rss/__init__.py
diff --git a/django/views/rss/rss.py b/django/views/rss/rss.py
new file mode 100644
index 0000000000..4f77307a91
--- /dev/null
+++ b/django/views/rss/rss.py
@@ -0,0 +1,12 @@
+from django.core import rss
+from django.core.exceptions import Http404
+from django.utils.httpwrappers import HttpResponse
+
+def feed(request, slug, param=None):
+ try:
+ f = rss.get_registered_feed(slug).get_feed(param)
+ except rss.FeedIsNotRegistered:
+ raise Http404
+ response = HttpResponse(mimetype='application/xml')
+ f.write(response, 'utf-8')
+ return response