diff options
author | Adrian Holovaty <adrian@holovaty.com> | 2005-07-13 01:25:57 +0000 |
---|---|---|
committer | Adrian Holovaty <adrian@holovaty.com> | 2005-07-13 01:25:57 +0000 |
commit | ed114e15106192b22ebb78ef5bf5bce72b419d13 (patch) | |
tree | f7c27f035cca8d50bd69e2ecbd7497fccec4a35a | |
parent | 07ffc7d605cc96557db28a9e35da69bc0719611b (diff) | |
download | django-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
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 ``&`` 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> </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 = ['(', '<', '<'] +TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>'] + +# list of possible strings used for bullets in bulleted lists +DOTS = ['·', '*', '\xe2\x80\xa2', '•', '•', '•'] + +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>(?: |\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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') + +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('&', 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> </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> </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&last_name=smith' + >>> get_query_string({'first_name': 'adrian', 'last_name': 'smith'}, {'first_name': 'john'}) + '?first_name=john&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 '?' + '&'.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> › %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">‹ %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">‹ %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">‹ 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 = ' ' + 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 › ') + 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(' <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> › <a href="../">%s</a> › %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 %%} <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 |