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
This commit is contained in:
parent
07ffc7d605
commit
ed114e1510
|
@ -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()
|
|
@ -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;"
|
|
@ -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])
|
|
@ -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)
|
|
@ -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',
|
||||
)
|
|
@ -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)
|
|
@ -0,0 +1 @@
|
|||
__all__ = ['{{ app_name }}']
|
|
@ -0,0 +1,3 @@
|
|||
from django.core import meta
|
||||
|
||||
# Create your models here.
|
|
@ -0,0 +1,5 @@
|
|||
from django.conf.urls.defaults import *
|
||||
|
||||
urlpatterns = patterns('{{ project_name }}.apps.{{ app_name }}.views',
|
||||
# (r'', ''),
|
||||
)
|
|
@ -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'
|
|
@ -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 = (
|
||||
)
|
|
@ -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
|
||||
|
|
@ -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)
|
|
@ -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'),
|
||||
)
|
|
@ -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'),
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
from django.conf.urls.defaults import *
|
||||
|
||||
urlpatterns = patterns('django.views',
|
||||
(r'^(?P<url>.*)$', 'core.flatfiles.flat_file'),
|
||||
)
|
|
@ -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'),
|
||||
)
|
|
@ -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'),
|
||||
)
|
|
@ -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'),
|
||||
)
|
|
@ -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)
|
|
@ -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
|
|
@ -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',
|
||||
}
|
|
@ -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',
|
||||
}
|
|
@ -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)
|
|
@ -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'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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'))
|
|
@ -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'))
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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}
|
|
@ -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)
|
|
@ -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.'
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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'))
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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)
|
|
@ -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()))
|
|
@ -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)
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue