diff --git a/django/bin/django-admin.py b/django/bin/django-admin.py index 0d021ba172..8cf042a5c1 100755 --- a/django/bin/django-admin.py +++ b/django/bin/django-admin.py @@ -11,6 +11,7 @@ ACTION_MAPPING = { 'init': management.init, 'inspectdb': management.inspectdb, 'install': management.install, + 'installperms': management.installperms, 'runserver': management.runserver, 'sql': management.get_sql_create, 'sqlall': management.get_sql_all, @@ -24,7 +25,7 @@ ACTION_MAPPING = { 'validate': management.validate, } -NO_SQL_TRANSACTION = ('adminindex', 'createcachetable', 'dbcheck', 'install', 'sqlindexes') +NO_SQL_TRANSACTION = ('adminindex', 'createcachetable', 'dbcheck', 'install', 'installperms', 'sqlindexes') def get_usage(): """ diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 52acb51de4..40e230b04c 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -65,8 +65,8 @@ TEMPLATE_FILE_EXTENSION = '.html' # See the comments in django/core/template/loader.py for interface # documentation. TEMPLATE_LOADERS = ( -# 'django.core.template.loaders.app_directories.load_template_source', 'django.core.template.loaders.filesystem.load_template_source', + 'django.core.template.loaders.app_directories.load_template_source', # 'django.core.template.loaders.eggs.load_template_source', ) diff --git a/django/contrib/admin/media/js/urlify.js b/django/contrib/admin/media/js/urlify.js index 1e0f04b9ab..5cdb6890c4 100644 --- a/django/contrib/admin/media/js/urlify.js +++ b/django/contrib/admin/media/js/urlify.js @@ -9,7 +9,7 @@ function URLify(s, num_chars) { s = s.replace(r, ''); s = s.replace(/[^\w\s-]/g, ''); // remove unneeded chars s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces - s = s.replace(/\s+/g, '_'); // convert spaces to underscores + s = s.replace(/\s+/g, '-'); // convert spaces to hyphens s = s.toLowerCase(); // convert to lowercase return s.substring(0, num_chars);// trim to first num_chars chars } \ No newline at end of file diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 23c998df68..84f1ea7b87 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -2,28 +2,28 @@ {% load admin_modify %} {% load adminmedia %} {% block extrahead %} -{% for js in javascript_imports %}{% include_admin_script js %}{% endfor %} +{% for js in bound_manipulator.javascript_imports %}{% include_admin_script js %}{% endfor %} {% endblock %} -{% block coltype %}{{ coltype }}{% endblock %} -{% block bodyclass %}{{app_label}}-{{object_name.lower}} change-form{% endblock %} +{% block coltype %}{{ bound_manipulator.coltype }}{% endblock %} +{% block bodyclass %}{{app_label}}-{{bound_manipulator.object_name.lower}} change-form{% endblock %} {% block breadcrumbs %}{% if not is_popup %} {% endif %}{% endblock %} {% block content %}
{% if change %}{% if not is_popup %} {% endif %}{% endif %} -
{% block form_top %}{%endblock%} +{% block form_top %}{%endblock%} {% if is_popup %}{% endif %} -{% if save_on_top %}{% submit_row %}{% endif %} +{% if bound_manipulator.save_on_top %}{% submit_row %}{% endif %} {% if form.error_dict %}

Please correct the error{{ form.error_dict.items|pluralize }} below.

{% endif %} -{% for bound_field_set in bound_field_sets %} +{% for bound_field_set in bound_manipulator.bound_field_sets %}
{% if bound_field_set.name %}

{{bound_field_set.name }}

{% endif %} {% for bound_field_line in bound_field_set %} @@ -36,7 +36,7 @@ {% endfor %} {% block after_field_sets %}{% endblock %} {% if change %} - {% if ordered_objects %} + {% if bound_manipulator.ordered_objects %}

Ordering

{% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %} @@ -45,23 +45,23 @@ {% endif %} {% endif %} -{% for related_object in inline_related_objects %}{% edit_inline related_object %}{% endfor %} +{% for related_object in bound_manipulator.inline_related_objects %}{% edit_inline related_object %}{% endfor %} {% block after_related_objects%}{%endblock%} {% submit_row %} {% if add %} - + {% endif %} -{% if auto_populated_fields %} +{% if bound_manipulator.auto_populated_fields %} {% endif %} {% if change %} - {% if ordered_objects %} + {% if bound_manipulator.ordered_objects %} {% if form.order_objects %}
    {% for object in form.order_objects %} -
  • - {{ object|truncatewords:"5" }} +
  • + {{ object|truncatewords:"5" }}
  • {% endfor%} {% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html new file mode 100644 index 0000000000..78b51a5916 --- /dev/null +++ b/django/contrib/admin/templates/admin/change_list.html @@ -0,0 +1,19 @@ +{% load admin_list %} +{% extends "admin/base_site" %} +{% block bodyclass %}change-list{% endblock %} +{% if not is_popup %}{% block breadcrumbs %}{% endblock %}{% endif %} +{% block coltype %}flex{% endblock %} +{% block content %} +
    +{%if has_add_permission %} + \n' +{% endif %} +
    +{% search_form cl%} +{% date_hierarchy cl%} +{% filters cl %} +{% result_list cl%} +{% pagination cl%} +
    +
    +{%endblock%} \ No newline at end of file diff --git a/django/contrib/admin/templates/admin/filters.html b/django/contrib/admin/templates/admin/filters.html new file mode 100644 index 0000000000..844571f08a --- /dev/null +++ b/django/contrib/admin/templates/admin/filters.html @@ -0,0 +1,5 @@ +{% if cl.has_filters %}
    +

    Filter

    +{% for spec in cl.filter_specs %} + {% output_filter_spec cl spec %} +{% endfor %}
    {% endif %} \ No newline at end of file diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html new file mode 100644 index 0000000000..fa023ec467 --- /dev/null +++ b/django/contrib/admin/templates/admin/pagination.html @@ -0,0 +1,13 @@ + +

    +{% if pagination_required %} +{%for i in page_range %} + {% paginator_number cl i %} +{%endfor%} +{%endif%} +{{cl.result_count}} {% ifequal cl.result_count 1 %}{{cl.opts.verbose_name}}{%else%}{{cl.opts.verbose_name_plural}}{%endifequal%} +{% if need_show_all_link %}  Show all{%endif%} + +

    + + \ No newline at end of file diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html new file mode 100644 index 0000000000..d93c069a54 --- /dev/null +++ b/django/contrib/admin/templates/admin/search_form.html @@ -0,0 +1,14 @@ +{%if cl.lookup_opts.admin.search_fields %} +
    + + + +{%if show_result_count %} + {{cl.result_count}}s result{{cl.result_count|pluralize}} ({{cl.full_result_count}} total) +{%endif%} +{% for pair in cl.params.items %} + {%ifnotequal pair.0 search_var%}{%endifnotequal%} +{% endfor %} +
    + +{%endif%} \ No newline at end of file diff --git a/django/contrib/admin/templates/admin/template_debug.html b/django/contrib/admin/templates/admin/template_debug.html index b923cee8f1..964bf747a3 100644 --- a/django/contrib/admin/templates/admin/template_debug.html +++ b/django/contrib/admin/templates/admin/template_debug.html @@ -7,9 +7,9 @@ {{message}} - {%ifnotequal bottom "0"%} - .
    .
    .
    - {%endifnotequal%} + {%if top%} + ... + {%endif%}
        
    @@ -17,8 +17,8 @@
        {% else %}{{source_line.0|rjust:"5"}}:{{ source_line.1 }} 
        {% endifequal %}{% endfor %}
        
    - {%ifnotequal top total%} - .
    .
    .
    + {%ifnotequal bottom total%} + ... {%endifnotequal%}
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py new file mode 100644 index 0000000000..d03d27b036 --- /dev/null +++ b/django/contrib/admin/templatetags/admin_list.py @@ -0,0 +1,280 @@ +from django.core.template.decorators import simple_tag, inclusion_tag + +from django.contrib.admin.views.main import MAX_SHOW_ALL_ALLOWED, DEFAULT_RESULTS_PER_PAGE, ALL_VAR, \ + ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR , SEARCH_VAR , IS_POPUP_VAR, EMPTY_CHANGELIST_VALUE + +from django.core import meta +from django.utils.text import capfirst +from django.utils.html import strip_tags, escape +from django.core.exceptions import ObjectDoesNotExist +from django.conf.settings import ADMIN_MEDIA_PREFIX +from django.core import template + +DOT = '.' + +class QueryStringNode(template.Node): + def __init__(self, cl_var, override_vars, remove_vars): + self.cl_var, self.override_vars, self.remove_vars = cl_var, override_vars, remove_vars + + def render(self, context): + def res(var): + return template.resolve_variable(var, context) + + cl = res(self.cl_var) + overrides = dict([ (res(k), res(v)) for k,v in self.override_vars ]) + remove = [res(v) for v in self.remove_vars] + return cl.get_query_string(overrides, remove) + +def do_query_string(parser, token): + bits = token.contents.split()[1:] + in_override = False + in_remove = False + override_vars = [] + remove_vars = [] + cl_var = bits.pop(0) + + for word in bits: + if in_override: + if word == 'remove': + in_remove = True + in_override = False + else: + override_vars.append(word.split(':')) + elif in_remove: + remove_vars.append(word) + else: + if word == 'override': + in_override = True + elif word == 'remove': + remove = True + + return QueryStringNode(cl_var, override_vars, remove_vars) + +template.register_tag('query_string', do_query_string) + + +#@simple_tag +def paginator_number(cl,i): + if i == DOT: + return '... ' + elif i == cl.page_num: + return '%d ' % (i+1) + else: + return '%d ' % (cl.get_query_string( {PAGE_VAR: i}), (i == paginator.pages-1 and ' class="end"' or ''), i+1) +paginator_number = simple_tag(paginator_number) + +#@inclusion_tag('admin/pagination') +def pagination(cl): + paginator, page_num = cl.paginator, cl.page_num + + pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page + if not pagination_required: + page_range = [] + else: + ON_EACH_SIDE = 3 + ON_ENDS = 2 + + # If there are 10 or fewer pages, display links to every page. + # Otherwise, do some fancy + if paginator.pages <= 10: + page_range = range(paginator.pages) + else: + # Insert "smart" pagination links, so that there are always ON_ENDS + # links at either end of the list of pages, and there are always + # ON_EACH_SIDE links at either end of the "current page" link. + page_range = [] + if page_num > (ON_EACH_SIDE + ON_ENDS): + page_range.extend(range(0, ON_EACH_SIDE - 1)) + page_range.append(DOT) + page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1)) + else: + page_range.extend(range(0, page_num + 1)) + if page_num < (paginator.pages - ON_EACH_SIDE - ON_ENDS - 1): + page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1)) + page_range.append(DOT) + page_range.extend(range(paginator.pages - ON_ENDS, paginator.pages)) + else: + page_range.extend(range(page_num + 1, paginator.pages)) + + return {'cl': cl, + 'pagination_required': pagination_required, + 'need_show_all_link': cl.can_show_all and not cl.show_all and cl.multi_page, + 'page_range': page_range, + 'ALL_VAR': ALL_VAR, + '1': 1 + } +pagination = inclusion_tag('admin/pagination')(pagination) + +#@simple_tag +def result_list(cl): + result_list, lookup_opts, mod, order_field, order_type, params, is_popup, opts = \ + cl.result_list, cl.lookup_opts, cl.mod, cl.order_field, cl.order_type, cl.params, cl.is_popup, cl.opts + + raw_template = [] + if result_list: + # Table headers. + raw_template.append('\n\n\n') + for i, field_name in enumerate(lookup_opts.admin.list_display): + try: + f = lookup_opts.get_field(field_name) + except meta.FieldDoesNotExist: + # For non-field list_display values, check for the function + # attribute "short_description". If that doesn't exist, fall + # back to the method name. And __repr__ is a special-case. + if field_name == '__repr__': + header = lookup_opts.verbose_name + else: + func = getattr(mod.Klass, field_name) # Let AttributeErrors propogate. + try: + header = func.short_description + except AttributeError: + header = func.__name__ + # Non-field list_display values don't get ordering capability. + raw_template.append('' % capfirst(header)) + else: + if isinstance(f.rel, meta.ManyToOne) and f.null: + raw_template.append('' % capfirst(f.verbose_name)) + else: + th_classes = [] + new_order_type = 'asc' + if field_name == order_field: + th_classes.append('sorted %sending' % order_type.lower()) + new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type.lower()] + raw_template.append('%s' % \ + ((th_classes and ' class="%s"' % ' '.join(th_classes) or ''), + cl.get_query_string( {ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}), + capfirst(f.verbose_name))) + raw_template.append('\n\n') + # Result rows. + pk = lookup_opts.pk.name + for i, result in enumerate(result_list): + raw_template.append('\n' % (i % 2 + 1)) + for j, field_name in enumerate(lookup_opts.admin.list_display): + row_class = '' + try: + f = lookup_opts.get_field(field_name) + except meta.FieldDoesNotExist: + # For non-field list_display values, the value is a method + # name. Execute the method. + try: + result_repr = strip_tags(str(getattr(result, field_name)())) + except ObjectDoesNotExist: + result_repr = EMPTY_CHANGELIST_VALUE + else: + field_val = getattr(result, f.column) + # Foreign-key fields are special: Use the repr of the + # related object. + if isinstance(f.rel, meta.ManyToOne): + if field_val is not None: + result_repr = getattr(result, 'get_%s' % f.name)() + else: + result_repr = EMPTY_CHANGELIST_VALUE + # Dates are special: They're formatted in a certain way. + elif isinstance(f, meta.DateField): + if field_val: + if isinstance(f, meta.DateTimeField): + result_repr = dateformat.format(field_val, 'N j, Y, P') + else: + result_repr = dateformat.format(field_val, 'N j, Y') + else: + result_repr = EMPTY_CHANGELIST_VALUE + row_class = ' class="nowrap"' + # Booleans are special: We use images. + elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField): + BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} + result_repr = '%s' % (ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val) + # ImageFields are special: Use a thumbnail. + elif isinstance(f, meta.ImageField): + from django.parts.media.photos import get_thumbnail_url + result_repr = '%s' % (get_thumbnail_url(getattr(result, 'get_%s_url' % f.name)(), '120'), field_val, field_val) + # FloatFields are special: Zero-pad the decimals. + elif isinstance(f, meta.FloatField): + if field_val is not None: + result_repr = ('%%.%sf' % f.decimal_places) % field_val + else: + result_repr = EMPTY_CHANGELIST_VALUE + # Fields with choices are special: Use the representation + # of the choice. + elif f.choices: + result_repr = dict(f.choices).get(field_val, EMPTY_CHANGELIST_VALUE) + else: + result_repr = strip_tags(str(field_val)) + # Some browsers don't like empty ""s. + if result_repr == '': + result_repr = ' ' + if j == 0: # First column is a special case + result_id = getattr(result, pk) + raw_template.append('%s' % \ + (row_class, result_id, (is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %r); return false;"' % result_id or ''), result_repr)) + else: + raw_template.append('%s' % (row_class, result_repr)) + raw_template.append('\n') + del result_list # to free memory + raw_template.append('
%s%s
\n') + else: + raw_template.append('

No %s matched your search criteria.

' % opts.verbose_name_plural) + return ''.join(raw_template) +result_list = simple_tag(result_list) + +#@simple_tag +def date_hierarchy(cl): + lookup_opts, params, lookup_params, lookup_mod = \ + cl.lookup_opts, cl.params, cl.lookup_params, cl.lookup_mod + + raw_template = [] + if lookup_opts.admin.date_hierarchy: + field_name = lookup_opts.admin.date_hierarchy + + year_field = '%s__year' % field_name + month_field = '%s__month' % field_name + day_field = '%s__day' % field_name + field_generic = '%s__' % field_name + year_lookup = params.get(year_field) + month_lookup = params.get(month_field) + day_lookup = params.get(day_field) + + raw_template.append('
\n
\n
\n') + return ''.join(raw_template) +date_hierarchy = simple_tag(date_hierarchy) + +#@inclusion_tag('admin/search_form') +def search_form(cl): + return { 'cl': cl, + 'show_result_count': cl.result_count != cl.full_result_count and not cl.opts.one_to_one_field, + 'search_var': SEARCH_VAR } +search_form = inclusion_tag('admin/search_form')(search_form) + +#@simple_tag +def output_filter_spec(cl, spec): + return spec.output(cl) +output_filter_spec = simple_tag(output_filter_spec) + +#@inclusion_tag('admin/filters') +def filters(cl): + return {'cl': cl} +filters = inclusion_tag('admin/filters')(filters) \ No newline at end of file diff --git a/django/contrib/admin/views/doc.py b/django/contrib/admin/views/doc.py index 166ecdff2d..e4d0f1c648 100644 --- a/django/contrib/admin/views/doc.py +++ b/django/contrib/admin/views/doc.py @@ -288,9 +288,9 @@ DATA_TYPE_MAPPING = { def get_readable_field_data_type(field): # ForeignKey is a special case. Use the field type of the relation. - if field.__class__.__name__ == 'ForeignKey': + if field.get_internal_type() == 'ForeignKey': field = field.rel.get_related_field() - return DATA_TYPE_MAPPING[field.__class__.__name__] % field.__dict__ + return DATA_TYPE_MAPPING[field.get_internal_type()] % field.__dict__ def extract_views_from_urlpatterns(urlpatterns, base=''): """ diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index e6bd1f5f08..3c8c150242 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,5 +1,4 @@ # Generic admin views. - from django.contrib.admin.views.decorators import staff_member_required from django.core import formfields, meta, template from django.core.template import loader @@ -12,7 +11,25 @@ from django.utils.html import strip_tags from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect from django.utils.text import capfirst, get_text_list from django.conf.settings import ADMIN_MEDIA_PREFIX +from django.core.paginator import ObjectPaginator, InvalidPage +from django.utils import dateformat +from django.utils.dates import MONTHS +from django.utils.html import escape import operator +import datetime + +# The system will display a "Show all" link only if the total result count +# is less than or equal to this setting. +MAX_SHOW_ALL_ALLOWED = 200 + +DEFAULT_RESULTS_PER_PAGE = 100 + +ALL_VAR = 'all' +ORDER_VAR = 'o' +ORDER_TYPE_VAR = 'ot' +PAGE_VAR = 'p' +SEARCH_VAR = 'q' +IS_POPUP_VAR = 'pop' # Text to display within changelist table cells if the value is blank. EMPTY_CHANGELIST_VALUE = '(None)' @@ -28,495 +45,354 @@ def _get_mod_opts(app_label, module_name): raise Http404 # This object is valid but has no admin interface. return mod, opts -def get_query_string(original_params, new_params={}, remove=[]): - """ - >>> get_query_string({'first_name': 'adrian', 'last_name': 'smith'}) - '?first_name=adrian&last_name=smith' - >>> get_query_string({'first_name': 'adrian', 'last_name': 'smith'}, {'first_name': 'john'}) - '?first_name=john&last_name=smith' - >>> get_query_string({'test': 'yes'}, {'blah': 'no'}, ['te']) - '?blah=no' - """ - p = original_params.copy() - for r in remove: - for k in p.keys(): - if k.startswith(r): - del p[k] - for k, v in new_params.items(): - if p.has_key(k) and v is None: - del p[k] - elif v is not None: - p[k] = v - return '?' + '&'.join(['%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20') def index(request): return render_to_response('admin/index', {'title': 'Site administration'}, context_instance=Context(request)) index = staff_member_required(index) -def change_list(request, app_label, module_name): - from django.core import paginator - from django.utils import dateformat - from django.utils.dates import MONTHS - from django.utils.html import escape - import datetime +class IncorrectLookupParameters(Exception): + pass - # The system will display a "Show all" link only if the total result count - # is less than or equal to this setting. - MAX_SHOW_ALL_ALLOWED = 200 +class FilterSpec(object): + filter_specs = [] + def __init__(self, f, request, params): + self.field = f + self.params = params + + def register(cls, test, factory): + cls.filter_specs.append( (test, factory) ) + register = classmethod(register) + + def create(cls, f, request, params): + for test, factory in cls.filter_specs: + if test(f): + return factory(f, request, params) + create = classmethod(create) + + def has_output(self): + return True + + def output(self, cl): + t = [] + if self.has_output(): + t.append('

By %s:

\n
    \n' % self.title) + + for choice in self.choices: + t.append('%r\n' % \ + (self.is_selected(choice) and ' class="selected"' or ''), + self.get_query_string(choice) , + self.get_display(choice) ) + t.append('
\n\n') + return "".join(t) + +class RelatedFilterSpec(FilterSpec): + + def __init__(self, f, request, params): + super(RelatedFilterSpec, self).__init__(f, request, params) + if isinstance(f, meta.ManyToManyField): + self.lookup_title = f.rel.to.verbose_name + else: + self.lookup_title = f.verbose_name + self.lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to.pk.name) + self.lookup_val = request.GET.get(self.lookup_kwarg, None) + self.lookup_choices = f.rel.to.get_model_module().get_list() + + def has_output(self): + return len(self.lookup_choices) > 1 + + def output(self, cl): + t = [] + if self.has_output(): + t.append('

By %s:

\n
    \n' % self.lookup_title) + t.append('All\n' % \ + ((self.lookup_val is None and ' class="selected"' or ''), + cl.get_query_string({}, [lookup_kwarg]))) + for val in lookup_choices: + pk_val = getattr(val, f.rel.to.pk.column) + t.append('%r\n' % \ + ((self.lookup_val == str(pk_val) and ' class="selected"' or ''), + cl.get_query_string( {self.lookup_kwarg: pk_val}), val)) + t.append('
\n\n') + return "".join(t) +FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) - DEFAULT_RESULTS_PER_PAGE = 100 +class ChoicesFilterSpec(FilterSpec): + + def __init__(self, f, request, params): + super(ChoicesFilterSpec, self).__init__(f, request, params) + self.lookup_kwarg = '%s__exact' % f.name + self.lookup_val = request.GET.get(lookup_kwarg, None) + + def output(self, cl): + t = [] + t.append('

By %s:

    \n' % self.field.verbose_name) + t.append('All\n' % \ + ((self.lookup_val is None and ' class="selected"' or ''), + cl.get_query_string( {}, [self.lookup_kwarg]))) + for k, v in f.choices: + t.append('%s' % \ + ((str(k) == self.lookup_val) and ' class="selected"' or '', + cl.get_query_string( {lookup_kwarg: k}), v)) + t.append('
\n\n') + return "".join(t) +FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec) - ALL_VAR = 'all' - ORDER_VAR = 'o' - ORDER_TYPE_VAR = 'ot' - PAGE_VAR = 'p' - SEARCH_VAR = 'q' - IS_POPUP_VAR = 'pop' +class DateFieldFilterSpec(FilterSpec): + + def __init__(self, f, request, params): + super(DateFieldFilterSpec, self).__init__(f, request, params) + + self.field_generic = '%s__' % self.field.name + + self.date_params = dict([(k, v) for k, v in params.items() if k.startswith(field_generic)]) + + today = datetime.date.today() + one_week_ago = today - datetime.timedelta(days=7) + today_str = isinstance(self.f, meta.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d') + + self.links = ( + ('Any date', {}), + ('Today', {'%s__year' % self.field.name: str(today.year), + '%s__month' % self.field.name: str(today.month), + '%s__day' % self.field.name: str(today.day)}), + ('Past 7 days', {'%s__gte' % self.field.name: one_week_ago.strftime('%Y-%m-%d'), + '%s__lte' % f.name: today_str}), + ('This month', {'%s__year' % self.field.name: str(today.year), + '%s__month' % f.name: str(today.month)}), + ('This year', {'%s__year' % self.field.name: str(today.year)}) + ) + + def output(self, cl): + t = [] + t.append('

By %s:

    \n' % self.field.verbose_name) + for title, param_dict in self.links: + t.append('%s\n' % \ + ((self.date_params == param_dict) and ' class="selected"' or '', + cl.get_query_string( param_dict, self.field_generic), title)) + t.append('
\n\n') + return "".join(t) +FilterSpec.register(lambda f: isinstance(f, meta.DateField), DateFieldFilterSpec) - mod, opts = _get_mod_opts(app_label, module_name) - if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): - raise PermissionDenied +class BooleanFieldFilterSpec(FilterSpec): + + def __init__(self, f, request, params): + super(BooleanFieldFilterSpec, self).__init__(f, request, params) + self.lookup_kwarg = '%s__exact' % f.name + self.lookup_kwarg2 = '%s__isnull' % f.name + self.lookup_val = request.GET.get(self.lookup_kwarg, None) + self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None) + + def output(self, cl): + t = [] + t.append('

By %s:

    \n' % self.field.verbose_name) + for k, v in (('All', None), ('Yes', '1'), ('No', '0')): + t.append('%s\n' % \ + (((self.lookup_val == v and not self.lookup_val2) and ' class="selected"' or ''), + cl.get_query_string( {self.lookup_kwarg: v}, [self.lookup_kwarg2]), k)) + if isinstance(self.field, meta.NullBooleanField): + t.append('%s\n' % \ + (((lookup_val2 == 'True') and ' class="selected"' or ''), + cl.get_query_string( {self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]), 'Unknown')) + t.append('
\n\n') + return "".join(t) +FilterSpec.register(lambda f: isinstance(f, meta.BooleanField) or + isinstance(f, meta.NullBooleanField), BooleanFieldFilterSpec) - lookup_mod, lookup_opts = mod, opts +class ChangeList(object): + def __init__(self, request, app_label, module_name): + self.get_modules_and_options(app_label, module_name, request) + self.get_search_parameters(request) + self.get_ordering() + self.query = request.GET.get(SEARCH_VAR,'') + self.get_lookup_params() + self.get_results(request) + self.title = (self.is_popup + and 'Select %s' % self.opts.verbose_name + or 'Select %s to change' % self.opts.verbose_name) + self.get_filters(request) + + def get_filters(self, request): + self.filter_specs = [] + + if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field: + filter_fields = [self.lookup_opts.get_field(field_name) \ + for field_name in self.lookup_opts.admin.list_filter] + for f in filter_fields: + spec = FilterSpec.create(f, request, self.params) + if spec.has_output(): + self.filter_specs.append(spec) + + self.has_filters = bool(self.filter_specs) + + def get_query_string(self, new_params={}, remove=[]): + p = self.params.copy() + for r in remove: + for k in p.keys(): + if k.startswith(r): + del p[k] + for k, v in new_params.items(): + if p.has_key(k) and v is None: + del p[k] + elif v is not None: + p[k] = v + return '?' + '&'.join(['%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20') + + + def get_modules_and_options(self, app_label, module_name, request): + self.mod, self.opts = _get_mod_opts(app_label, module_name) + if not request.user.has_perm(app_label + '.' + self.opts.get_change_permission()): + raise PermissionDenied + + if self.opts.one_to_one_field: + self.lookup_mod = self.opts.one_to_one_field.rel.to.get_model_module() + self.lookup_opts = self.lookup_mod.Klass._meta + # If lookup_opts doesn't have admin set, give it the default meta.Admin(). + if not self.lookup_opts.admin: + self.lookup_opts.admin = meta.Admin() + else: + self.lookup_mod, self.lookup_opts = self.mod, self.opts + - if opts.one_to_one_field: - lookup_mod = opts.one_to_one_field.rel.to.get_model_module() - lookup_opts = lookup_mod.Klass._meta - # If lookup_opts doesn't have admin set, give it the default meta.Admin(). - if not lookup_opts.admin: - lookup_opts.admin = meta.Admin() - - # Get search parameters from the query string. - try: - page_num = int(request.GET.get(PAGE_VAR, 0)) - except ValueError: - page_num = 0 - show_all = request.GET.has_key(ALL_VAR) - is_popup = request.GET.has_key(IS_POPUP_VAR) - params = dict(request.GET.copy()) - if params.has_key(PAGE_VAR): - del params[PAGE_VAR] - # For ordering, first check the "ordering" parameter in the admin options, - # then check the object's default ordering. If neither of those exist, - # order descending by ID by default. Finally, look for manually-specified - # ordering from the query string. - ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] - - # Normalize it to new-style ordering. - ordering = meta.handle_legacy_orderlist(ordering) - - if ordering[0].startswith('-'): - order_field, order_type = ordering[0][1:], 'desc' - else: - order_field, order_type = ordering[0], 'asc' - if params.has_key(ORDER_VAR): + def get_search_parameters(self, request): + # Get search parameters from the query string. try: + self.req_get = request.GET + self.page_num = int(request.GET.get(PAGE_VAR, 0)) + except ValueError: + self.page_num = 0 + self.show_all = request.GET.has_key(ALL_VAR) + self.is_popup = request.GET.has_key(IS_POPUP_VAR) + self.params = dict(request.GET.copy()) + if self.params.has_key(PAGE_VAR): + del self.params[PAGE_VAR] + + def get_results(self, request): + lookup_mod, lookup_params, show_all, page_num = \ + self.lookup_mod, self.lookup_params, self.show_all, self.page_num + # Get the results. + try: + paginator = ObjectPaginator(lookup_mod, lookup_params, DEFAULT_RESULTS_PER_PAGE) + # Naked except! Because we don't have any other way of validating "params". + # They might be invalid if the keyword arguments are incorrect, or if the + # values are not in the correct type (which would result in a database + # error). + except: + raise IncorrectLookupParameters() + + # Get the total number of objects, with no filters applied. + real_lookup_params = lookup_params.copy() + del real_lookup_params['order_by'] + if real_lookup_params: + full_result_count = lookup_mod.get_count() + else: + full_result_count = paginator.hits + del real_lookup_params + result_count = paginator.hits + can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED + multi_page = result_count > DEFAULT_RESULTS_PER_PAGE + + # Get the list of objects to display on this page. + if (show_all and can_show_all) or not multi_page: + result_list = lookup_mod.get_list(**lookup_params) + else: try: - f = lookup_opts.get_field(lookup_opts.admin.list_display[int(params[ORDER_VAR])]) - except meta.FieldDoesNotExist: - pass - else: - if not isinstance(f.rel, meta.ManyToOne) or not f.null: - order_field = f.name - except (IndexError, ValueError): - pass # Invalid ordering specified. Just use the default. - if params.has_key(ORDER_TYPE_VAR) and params[ORDER_TYPE_VAR] in ('asc', 'desc'): - order_type = params[ORDER_TYPE_VAR] - query = request.GET.get(SEARCH_VAR, '') - - # Prepare the lookup parameters for the API lookup. - lookup_params = params.copy() - for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): - if lookup_params.has_key(i): - del lookup_params[i] - # If the order-by field is a field with a relationship, order by the value - # in the related table. - lookup_order_field = order_field - if isinstance(lookup_opts.get_field(order_field).rel, meta.ManyToOne): - f = lookup_opts.get_field(order_field) - rel_ordering = f.rel.to.ordering and f.rel.to.ordering[0] or f.rel.to.pk.column - lookup_order_field = '%s.%s' % (f.rel.to.db_table, rel_ordering) - # Use select_related if one of the list_display options is a field with a - # relationship. - for field_name in lookup_opts.admin.list_display: - try: - f = lookup_opts.get_field(field_name) - except meta.FieldDoesNotExist: - pass + result_list = p.get_page(page_num) + except InvalidPage: + result_list = [] + (self.result_count, self.full_result_count, self.result_list, + self.can_show_all, self.multi_page, self.paginator) = (result_count, + full_result_count, result_list, can_show_all, multi_page, paginator ) + + def get_ordering(self): + lookup_opts, params = self.lookup_opts, self.params + # For ordering, first check the "ordering" parameter in the admin options, + # then check the object's default ordering. If neither of those exist, + # order descending by ID by default. Finally, look for manually-specified + # ordering from the query string. + ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] + + # Normalize it to new-style ordering. + ordering = meta.handle_legacy_orderlist(ordering) + + if ordering[0].startswith('-'): + order_field, order_type = ordering[0][1:], 'desc' else: - if isinstance(f.rel, meta.ManyToOne): - lookup_params['select_related'] = True - break - lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,) - if lookup_opts.admin.search_fields and query: - or_queries = [] - for bit in query.split(): - or_query = [] - for field_name in lookup_opts.admin.search_fields: - or_query.append(('%s__icontains' % field_name, bit)) - or_queries.append(or_query) - lookup_params['_or'] = or_queries - - if opts.one_to_one_field: - lookup_params.update(opts.one_to_one_field.rel.limit_choices_to) - - # Get the results. - try: - p = paginator.ObjectPaginator(lookup_mod, lookup_params, DEFAULT_RESULTS_PER_PAGE) - # Naked except! Because we don't have any other way of validating "params". - # They might be invalid if the keyword arguments are incorrect, or if the - # values are not in the correct type (which would result in a database - # error). - except: - return HttpResponseRedirect(request.path) - - # Get the total number of objects, with no filters applied. - real_lookup_params = lookup_params.copy() - del real_lookup_params['order_by'] - if real_lookup_params: - full_result_count = lookup_mod.get_count() - else: - full_result_count = p.hits - del real_lookup_params - result_count = p.hits - can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED - multi_page = result_count > DEFAULT_RESULTS_PER_PAGE - - # Get the list of objects to display on this page. - if (show_all and can_show_all) or not multi_page: - result_list = lookup_mod.get_list(**lookup_params) - else: - try: - result_list = p.get_page(page_num) - except paginator.InvalidPage: - result_list = [] - - # Calculate filters first, because a CSS class high in the document depends - # on whether they are available. - filter_template = [] - if lookup_opts.admin.list_filter and not opts.one_to_one_field: - filter_fields = [lookup_opts.get_field(field_name) for field_name in lookup_opts.admin.list_filter] - for f in filter_fields: - # Many-to-many or many-to-one filter. - if f.rel: - if isinstance(f, meta.ManyToManyField): - lookup_title = f.rel.to.verbose_name + order_field, order_type = ordering[0], 'asc' + if params.has_key(ORDER_VAR): + try: + try: + f = lookup_opts.get_field(lookup_opts.admin.list_display[int(params[ORDER_VAR])]) + except meta.FieldDoesNotExist: + pass else: - lookup_title = f.verbose_name - lookup_kwarg = '%s__%s__exact' % (f.name, f.rel.to.pk.name) - lookup_val = request.GET.get(lookup_kwarg, None) - lookup_choices = f.rel.to.get_model_module().get_list() - if len(lookup_choices) > 1: - filter_template.append('

By %s:

\n
    \n' % lookup_title) - filter_template.append('All\n' % \ - ((lookup_val is None and ' class="selected"' or ''), - get_query_string(params, {}, [lookup_kwarg]))) - for val in lookup_choices: - pk_val = getattr(val, f.rel.to.pk.column) - filter_template.append('%r\n' % \ - ((lookup_val == str(pk_val) and ' class="selected"' or ''), - get_query_string(params, {lookup_kwarg: pk_val}), val)) - filter_template.append('
\n\n') - # Field with choices. - elif f.choices: - lookup_kwarg = '%s__exact' % f.name - lookup_val = request.GET.get(lookup_kwarg, None) - filter_template.append('

By %s:

    \n' % f.verbose_name) - filter_template.append('All\n' % \ - ((lookup_val is None and ' class="selected"' or ''), - get_query_string(params, {}, [lookup_kwarg]))) - for k, v in f.choices: - filter_template.append('%s' % \ - ((str(k) == lookup_val) and ' class="selected"' or '', - get_query_string(params, {lookup_kwarg: k}), v)) - filter_template.append('
\n\n') - # Date filter. - elif isinstance(f, meta.DateField): - today = datetime.date.today() - one_week_ago = today - datetime.timedelta(days=7) - field_generic = '%s__' % f.name - filter_template.append('

By %s:

    \n' % f.verbose_name) - date_params = dict([(k, v) for k, v in params.items() if k.startswith(field_generic)]) - today_str = isinstance(f, meta.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d') - for title, param_dict in ( - ('Any date', {}), - ('Today', {'%s__year' % f.name: str(today.year), '%s__month' % f.name: str(today.month), '%s__day' % f.name: str(today.day)}), - ('Past 7 days', {'%s__gte' % f.name: one_week_ago.strftime('%Y-%m-%d'), '%s__lte' % f.name: today_str}), - ('This month', {'%s__year' % f.name: str(today.year), '%s__month' % f.name: str(today.month)}), - ('This year', {'%s__year' % f.name: str(today.year)}) - ): - filter_template.append('%s\n' % \ - ((date_params == param_dict) and ' class="selected"' or '', - get_query_string(params, param_dict, field_generic), title)) - filter_template.append('
\n\n') - elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField): - lookup_kwarg = '%s__exact' % f.name - lookup_kwarg2 = '%s__isnull' % f.name - lookup_val = request.GET.get(lookup_kwarg, None) - lookup_val2 = request.GET.get(lookup_kwarg2, None) - filter_template.append('

By %s:

    \n' % f.verbose_name) - for k, v in (('All', None), ('Yes', '1'), ('No', '0')): - filter_template.append('%s\n' % \ - (((lookup_val == v and not lookup_val2) and ' class="selected"' or ''), - get_query_string(params, {lookup_kwarg: v}, [lookup_kwarg2]), k)) - if isinstance(f, meta.NullBooleanField): - filter_template.append('%s\n' % \ - (((lookup_val2 == 'True') and ' class="selected"' or ''), - get_query_string(params, {lookup_kwarg2: 'True'}, [lookup_kwarg]), 'Unknown')) - filter_template.append('
\n\n') - else: - pass # Invalid argument to "list_filter" - - raw_template = ['{% extends "admin/base_site" %}\n'] - raw_template.append('{% block bodyclass %}change-list{% endblock %}\n') - if not is_popup: - raw_template.append('{%% block breadcrumbs %%}{%% endblock %%}\n' % capfirst(opts.verbose_name_plural)) - raw_template.append('{% block coltype %}flex{% endblock %}') - raw_template.append('{% block content %}\n') - raw_template.append('
\n') - if request.user.has_perm(app_label + '.' + lookup_opts.get_add_permission()): - raw_template.append('\n' % ((is_popup and '?_popup=1' or ''), opts.verbose_name)) - raw_template.append('
\n' % (filter_template and ' filtered' or '')) - - # Search form. - if lookup_opts.admin.search_fields: - raw_template.append('
\n
\n') - raw_template.append('') - - # Date-based navigation. - if lookup_opts.admin.date_hierarchy: - field_name = lookup_opts.admin.date_hierarchy - - year_field = '%s__year' % field_name - month_field = '%s__month' % field_name - day_field = '%s__day' % field_name - field_generic = '%s__' % field_name - year_lookup = params.get(year_field) - month_lookup = params.get(month_field) - day_lookup = params.get(day_field) - - raw_template.append('
\n
\n
\n') - - # Filters. - if filter_template: - raw_template.append('
\n

Filter

\n') - raw_template.extend(filter_template) - raw_template.append('
') - del filter_template - - # Result table. - if result_list: - # Table headers. - raw_template.append('\n\n\n') - for i, field_name in enumerate(lookup_opts.admin.list_display): + if not isinstance(f.rel, meta.ManyToOne) or not f.null: + order_field = f.name + except (IndexError, ValueError): + pass # Invalid ordering specified. Just use the default. + if params.has_key(ORDER_TYPE_VAR) and params[ORDER_TYPE_VAR] in ('asc', 'desc'): + order_type = params[ORDER_TYPE_VAR] + self.order_field, self.order_type = order_field, order_type + + def get_lookup_params(self): + # Prepare the lookup parameters for the API lookup. + (params, order_field, lookup_opts, order_type, opts, query) = \ + (self.params, self.order_field, self.lookup_opts, self.order_type, self.opts, self.query) + + lookup_params = params.copy() + for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): + if lookup_params.has_key(i): + del lookup_params[i] + # If the order-by field is a field with a relationship, order by the value + # in the related table. + lookup_order_field = order_field + if isinstance(lookup_opts.get_field(order_field).rel, meta.ManyToOne): + f = lookup_opts.get_field(order_field) + rel_ordering = f.rel.to.ordering and f.rel.to.ordering[0] or f.rel.to.pk.column + lookup_order_field = '%s.%s' % (f.rel.to.db_table, rel_ordering) + # Use select_related if one of the list_display options is a field with a + # relationship. + for field_name in lookup_opts.admin.list_display: try: f = lookup_opts.get_field(field_name) except meta.FieldDoesNotExist: - # For non-field list_display values, check for the function - # attribute "short_description". If that doesn't exist, fall - # back to the method name. And __repr__ is a special-case. - if field_name == '__repr__': - header = lookup_opts.verbose_name - else: - func = getattr(mod.Klass, field_name) # Let AttributeErrors propogate. - try: - header = func.short_description - except AttributeError: - header = func.__name__ - # Non-field list_display values don't get ordering capability. - raw_template.append('' % capfirst(header)) + pass else: - if isinstance(f.rel, meta.ManyToOne) and f.null: - raw_template.append('' % capfirst(f.verbose_name)) - else: - th_classes = [] - new_order_type = 'asc' - if field_name == order_field: - th_classes.append('sorted %sending' % order_type.lower()) - new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type.lower()] - raw_template.append('%s' % \ - ((th_classes and ' class="%s"' % ' '.join(th_classes) or ''), - get_query_string(params, {ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}), - capfirst(f.verbose_name))) - raw_template.append('\n\n') - # Result rows. - pk = lookup_opts.pk.name - for i, result in enumerate(result_list): - raw_template.append('\n' % (i % 2 + 1)) - for j, field_name in enumerate(lookup_opts.admin.list_display): - row_class = '' - try: - f = lookup_opts.get_field(field_name) - except meta.FieldDoesNotExist: - # For non-field list_display values, the value is a method - # name. Execute the method. - try: - result_repr = strip_tags(str(getattr(result, field_name)())) - except ObjectDoesNotExist: - result_repr = EMPTY_CHANGELIST_VALUE - else: - field_val = getattr(result, f.column) - # Foreign-key fields are special: Use the repr of the - # related object. - if isinstance(f.rel, meta.ManyToOne): - if field_val is not None: - result_repr = getattr(result, 'get_%s' % f.name)() - else: - result_repr = EMPTY_CHANGELIST_VALUE - # Dates are special: They're formatted in a certain way. - elif isinstance(f, meta.DateField): - if field_val: - if isinstance(f, meta.DateTimeField): - result_repr = dateformat.format(field_val, 'N j, Y, P') - else: - result_repr = dateformat.format(field_val, 'N j, Y') - else: - result_repr = EMPTY_CHANGELIST_VALUE - row_class = ' class="nowrap"' - # Booleans are special: We use images. - elif isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField): - BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} - result_repr = '%s' % (ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val) - # ImageFields are special: Use a thumbnail. - elif isinstance(f, meta.ImageField): - from django.parts.media.photos import get_thumbnail_url - result_repr = '%s' % (get_thumbnail_url(getattr(result, 'get_%s_url' % f.name)(), '120'), field_val, field_val) - # FloatFields are special: Zero-pad the decimals. - elif isinstance(f, meta.FloatField): - if field_val is not None: - result_repr = ('%%.%sf' % f.decimal_places) % field_val - else: - result_repr = EMPTY_CHANGELIST_VALUE - # Fields with choices are special: Use the representation - # of the choice. - elif f.choices: - result_repr = dict(f.choices).get(field_val, EMPTY_CHANGELIST_VALUE) - else: - result_repr = strip_tags(str(field_val)) - # Some browsers don't like empty ""s. - if result_repr == '': - result_repr = ' ' - if j == 0: # First column is a special case - result_id = getattr(result, pk) - raw_template.append('%s' % \ - (row_class, result_id, (is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %r); return false;"' % result_id or ''), result_repr)) - else: - raw_template.append('%s' % (row_class, result_repr)) - raw_template.append('\n') - del result_list # to free memory - raw_template.append('
%s%s
\n') - else: - raw_template.append('

No %s matched your search criteria.

' % opts.verbose_name_plural) + if isinstance(f.rel, meta.ManyToOne): + lookup_params['select_related'] = True + break + lookup_params['order_by'] = ((order_type == 'desc' and '-' or '') + lookup_order_field,) + if lookup_opts.admin.search_fields and query: + or_queries = [] + for bit in query.split(): + or_query = [] + for field_name in lookup_opts.admin.search_fields: + or_query.append(('%s__icontains' % field_name, bit)) + or_queries.append(or_query) + lookup_params['_or'] = or_queries + + if opts.one_to_one_field: + lookup_params.update(opts.one_to_one_field.rel.limit_choices_to) + self.lookup_params = lookup_params + - # Pagination. - raw_template.append('

') - if (show_all and can_show_all) or not multi_page: - pass - else: - raw_template.append('Page › ') - ON_EACH_SIDE = 3 - ON_ENDS = 2 - DOT = '.' - # If there are 10 or fewer pages, display links to every page. - # Otherwise, do some fancy - if p.pages <= 10: - page_range = range(p.pages) - else: - # Insert "smart" pagination links, so that there are always ON_ENDS - # links at either end of the list of pages, and there are always - # ON_EACH_SIDE links at either end of the "current page" link. - page_range = [] - if page_num > (ON_EACH_SIDE + ON_ENDS): - page_range.extend(range(0, ON_EACH_SIDE - 1)) - page_range.append(DOT) - page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1)) - else: - page_range.extend(range(0, page_num + 1)) - if page_num < (p.pages - ON_EACH_SIDE - ON_ENDS - 1): - page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1)) - page_range.append(DOT) - page_range.extend(range(p.pages - ON_ENDS, p.pages)) - else: - page_range.extend(range(page_num + 1, p.pages)) - for i in page_range: - if i == DOT: - raw_template.append('... ') - elif i == page_num: - raw_template.append('%d ' % (i+1)) - else: - raw_template.append('%d ' % \ - (get_query_string(params, {PAGE_VAR: i}), (i == p.pages-1 and ' class="end"' or ''), i+1)) - raw_template.append('%s %s' % (result_count, result_count == 1 and opts.verbose_name or opts.verbose_name_plural)) - if can_show_all and not show_all and multi_page: - raw_template.append('  Show all' % \ - get_query_string(params, {ALL_VAR: ''})) - raw_template.append('

') - - raw_template.append('
\n
') - raw_template.append('{% endblock %}\n') - t = loader.get_template_from_string(''.join(raw_template)) +def change_list(request, app_label, module_name): + try: + cl = ChangeList(request, app_label, module_name) + except IncorrectLookupParameters: + return HttpResponseRedirect(request.path) + c = Context(request, { - 'title': (is_popup and 'Select %s' % opts.verbose_name or 'Select %s to change' % opts.verbose_name), - 'is_popup': is_popup, + 'title': cl.title, + 'is_popup': cl.is_popup, + 'cl' : cl }) - return HttpResponse(t.render(c)) + return render_to_response('admin/change_list', + context_instance = c) change_list = staff_member_required(change_list) + use_raw_id_admin = lambda field: isinstance(field.rel, (meta.ManyToOne, meta.ManyToMany)) and field.rel.raw_id_admin -def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects): - t = ['
'] - if change or show_delete: - t.append('{%% if perms.%s.%s %%}{%% if not is_popup %%}

Delete

{%% endif %%}{%% endif %%}' % \ - (app_label, opts.get_delete_permission())) - if change and opts.admin.save_as: - t.append('{%% if not is_popup %%}{%% endif %%}' % \ - (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) - if not opts.admin.save_as or add: - t.append('{%% if not is_popup %%}{%% endif %%}' % \ - (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) - t.append('{%% if not is_popup %%}{%% endif %%}' % \ - (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) - t.append('' % \ - (ordered_objects and change and 'onclick="submitOrderForm();"' or '')) - t.append('
\n') - return t def get_javascript_imports(opts,auto_populated_fields, ordered_objects, field_sets): # Put in any necessary JavaScript imports. @@ -534,17 +410,17 @@ def get_javascript_imports(opts,auto_populated_fields, ordered_objects, field_se if not seen_collapse and 'collapse' in field_set.classes: seen_collapse = True js.append('js/admin/CollapsedFieldsets.js' ) - try: - for field_line in field_set: + + for field_line in field_set: + try: for f in field_line: if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface: js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) raise StopIteration - except StopIteration: - break + except StopIteration: + break return js - class AdminBoundField(BoundField): def __init__(self, field, field_mapping, original): super(AdminBoundField, self).__init__(field,field_mapping,original) @@ -606,48 +482,48 @@ class AdminBoundFieldSet(BoundFieldSet): def __init__(self, field_set, field_mapping, original): super(AdminBoundFieldSet, self).__init__(field_set, field_mapping, original, AdminBoundFieldLine) + +class AdminBoundManipulator(object): + def __init__(self, opts, manipulator, field_mapping): + self.inline_related_objects = opts.get_followed_related_objects() -def render_change_form(opts, app_label, context, add=False, change=False, show_delete=False, form_url=''): - ordered_objects = opts.get_ordered_objects()[:] - auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] - coltype = ordered_objects and 'colMS' or 'colM' - - has_absolute_url = hasattr(opts.get_model_module().Klass, 'get_absolute_url') - form_enc_attrib = opts.has_field_type(meta.FileField) and 'enctype="multipart/form-data" ' or '' - form = context['form'] - original = context['original'] + field_sets = opts.admin.get_field_sets(opts) + self.original = hasattr(manipulator, 'original_object') and manipulator.original_object or None + self.bound_field_sets = [field_set.bind(field_mapping, self.original, AdminBoundFieldSet) + for field_set in field_sets] + + + self.ordered_objects = opts.get_ordered_objects()[:] + self.auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] + self.javascript_imports = get_javascript_imports(opts, self.auto_populated_fields, self.ordered_objects, field_sets); + + self.coltype = self.ordered_objects and 'colMS' or 'colM' + self.has_absolute_url = hasattr(opts.get_model_module().Klass, 'get_absolute_url') + self.form_enc_attrib = opts.has_field_type(meta.FileField) and \ + 'enctype="multipart/form-data" ' or '' + + + + self.first_form_field_id = self.bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id(); + self.ordered_object_names = ' '.join(['object.%s' % o.pk.name for o in self.ordered_objects]) + + self.save_on_top = opts.admin.save_on_top + self.save_as = opts.admin.save_as + + self.content_type_id = opts.get_content_type_id() + self.verbose_name_plural = opts.verbose_name_plural + self.verbose_name = opts.verbose_name + self.object_name = opts.object_name + +def render_change_form(opts, manipulator, app_label, context, add=False, change=False, show_delete=False, form_url=''): - field_sets = opts.admin.get_field_sets(opts) - bound_field_sets = [field_set.bind(form, original, AdminBoundFieldSet) - for field_set in field_sets] - - javascript_imports = get_javascript_imports(opts, auto_populated_fields, ordered_objects, field_sets); - first_form_field = bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0]; - inline_related_objects = opts.get_followed_related_objects() - ordered_object_names = ' '.join(['object.%s' % o.pk.name for o in ordered_objects]) - extra_context = { 'add': add, 'change': change, - 'first_form_field_id': first_form_field.get_id(), - 'ordered_objects' : ordered_objects, - 'ordered_object_names' : ordered_object_names, - 'auto_populated_fields' : auto_populated_fields, - 'javascript_imports' : javascript_imports, - 'coltype' : coltype, - 'has_absolute_url': has_absolute_url, - 'form_enc_attrib': form_enc_attrib, + 'bound_manipulator' : AdminBoundManipulator(opts, manipulator, context['form']), + 'has_delete_permission' : context['perms'][app_label][opts.get_delete_permission()], 'form_url' : form_url, - 'bound_field_sets' : bound_field_sets, - 'inline_related_objects': inline_related_objects, - 'content_type_id' : opts.get_content_type_id(), - 'save_on_top' : opts.admin.save_on_top, - 'verbose_name_plural': opts.verbose_name_plural, - 'verbose_name': opts.verbose_name, - 'save_as': opts.admin.save_as, 'app_label': app_label, - 'object_name': opts.object_name, - 'has_delete_permission' : context['perms'][app_label][opts.get_delete_permission()] } context.update(extra_context) @@ -677,7 +553,7 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p new_object = manipulator.save(new_data) log_add_message(request.user, opts,manipulator,new_object) msg = 'The %s "%s" was added successfully.' % (opts.verbose_name, new_object) - + pk_value = getattr(new_object,opts.pk.column) # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. if request.POST.has_key("_continue"): @@ -713,7 +589,7 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p if object_id_override is not None: c['object_id'] = object_id_override - return render_change_form(opts, app_label, c, add=True) + return render_change_form(opts, manipulator, app_label, c, add=True) add_stage = staff_member_required(add_stage) def log_change_message(user, opts,manipulator,new_object): @@ -754,6 +630,7 @@ def change_stage(request, app_label, module_name, object_id): new_object = manipulator.save(new_data) log_change_message(request.user,opts,manipulator,new_object) msg = 'The %s "%s" was changed successfully.' % (opts.verbose_name, new_object) + pk_value = getattr(new_object,opts.pk.column) if request.POST.has_key("_continue"): request.user.add_message("%s You may edit it again below." % msg) if request.REQUEST.has_key('_popup'): @@ -779,7 +656,7 @@ def change_stage(request, app_label, module_name, object_id): id_order_list = [] for rel_obj in opts.get_ordered_objects(): - id_order_list.extend(getattr(obj, 'get_%s_order' % rel_obj.object_name.lower())()) + id_order_list.extend(getattr(manipulator.original_object, 'get_%s_order' % rel_obj.object_name.lower())()) if id_order_list: new_data['order_'] = ','.join(map(str, id_order_list)) errors = {} @@ -794,7 +671,7 @@ def change_stage(request, app_label, module_name, object_id): wrt = related.opts.order_with_respect_to if wrt and wrt.rel and wrt.rel.to == opts: func = getattr(manipulator.original_object, 'get_%s_list' % - opts.get_rel_object_method_name(rel_opts, rel_field)) + related.get_method_name_part()) orig_list = func() form.order_objects.extend(orig_list) @@ -806,7 +683,7 @@ def change_stage(request, app_label, module_name, object_id): 'is_popup' : request.REQUEST.has_key('_popup') }) - return render_change_form(opts, app_label, c, change=True) + return render_change_form(opts,manipulator, app_label, c, change=True) @@ -825,8 +702,8 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current for related in opts.get_all_related_objects(): if related.opts in objects_seen: continue - objects_seen.append(rel_opts) - rel_opts_name = opts.get_rel_object_method_name(related.opts, related.field) + objects_seen.append(related.opts) + rel_opts_name = related.get_method_name_part() if isinstance(related.field.rel, meta.OneToOne): try: sub_obj = getattr(obj, 'get_%s' % rel_opts_name)() @@ -868,29 +745,29 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission()) if not user.has_perm(p): perms_needed.add(rel_opts.verbose_name) - for rel_opts, rel_field in opts.get_all_related_many_to_many_objects(): - if rel_opts in objects_seen: + for related in opts.get_all_related_many_to_many_objects(): + if related.opts in objects_seen: continue - objects_seen.append(rel_opts) - rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field) + objects_seen.append(related.opts) + rel_opts_name = related.get_method_name_part() has_related_objs = False for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)(): has_related_objs = True - if rel_field.rel.edit_inline or not rel_opts.admin: + if related.field.rel.edit_inline or not related.opts.admin: # Don't display link to edit, because it either has no # admin or is edited inline. nh(deleted_objects, current_depth, ['One or more %s in %s: %s' % \ - (rel_field.name, rel_opts.verbose_name, strip_tags(repr(sub_obj))), []]) + (related.field.name, related.opts.verbose_name, strip_tags(repr(sub_obj))), []]) else: # Display a link to the admin page. nh(deleted_objects, current_depth, ['One or more %s in %s: %s' % \ - (rel_field.name, rel_opts.verbose_name, rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []]) + (related.field.name, related.opts.verbose_name, related.opts.app_label, related.opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []]) # If there were related objects, and the user doesn't have # permission to change them, add the missing perm to perms_needed. - if rel_opts.admin and has_related_objs: - p = '%s.%s' % (rel_opts.app_label, rel_opts.get_change_permission()) + if related.opts.admin and has_related_objs: + p = '%s.%s' % (related.opts.app_label, related.opts.get_change_permission()) if not user.has_perm(p): - perms_needed.add(rel_opts.verbose_name) + perms_needed.add(related.opts.verbose_name) def delete_stage(request, app_label, module_name, object_id): import sets diff --git a/django/core/cache.py b/django/core/cache.py index 6391304158..caeb315f05 100644 --- a/django/core/cache.py +++ b/django/core/cache.py @@ -13,7 +13,9 @@ 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. + on localhost port 11211. You can use + multiple memcached servers by separating + them with semicolons. db://tablename/ A database backend in a table named "tablename". This table should be created @@ -134,7 +136,7 @@ else: "Memcached cache backend." def __init__(self, server, params): _Cache.__init__(self, params) - self._cache = memcache.Client([server]) + self._cache = memcache.Client(server.split(';')) def get(self, key, default=None): val = self._cache.get(key) diff --git a/django/core/formfields.py b/django/core/formfields.py index ee42cc5dd5..e4fcd5e8dd 100644 --- a/django/core/formfields.py +++ b/django/core/formfields.py @@ -315,6 +315,7 @@ class FormField: #################### class TextField(FormField): + input_type = "text" 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 @@ -878,8 +879,10 @@ class CommaSeparatedIntegerField(TextField): except validators.ValidationError, e: raise validators.CriticalValidationError, e.messages +class RawIdAdminField(CommaSeparatedIntegerField): def html2python(data): return data.split(','); + html2python = classmethod(html2python) class XMLLargeTextField(LargeTextField): """ diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 9b541b36db..190a8b02c2 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -48,6 +48,9 @@ class BaseHandler: from django.core.mail import mail_admins from django.conf.settings import DEBUG, INTERNAL_IPS, ROOT_URLCONF + # Reset query list per request. + db.db.queries = [] + # Apply request middleware for middleware_method in self._request_middleware: response = middleware_method(request) diff --git a/django/core/management.py b/django/core/management.py index c522cd9612..eac5f081b3 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -345,6 +345,27 @@ The full error: %s\n""" % \ install.help_doc = "Executes ``sqlall`` for the given model module name(s) in the current database." install.args = APP_ARGS +def installperms(mod): + "Installs any permissions for the given model, if needed." + from django.models.auth import permissions + from django.models.core import packages + num_added = 0 + package = packages.get_object(pk=mod._MODELS[0]._meta.app_label) + for klass in mod._MODELS: + opts = klass._meta + for codename, name in _get_all_permissions(opts): + try: + permissions.get_object(name__exact=name, codename__exact=codename, package__label__exact=package.label) + except permissions.PermissionDoesNotExist: + p = permissions.Permission(name=name, package=package, codename=codename) + p.save() + print "Added permission '%r'." % p + num_added += 1 + if not num_added: + print "No permissions were added, because all necessary permissions were already installed." +installperms.help_doc = "Installs any permissions for the given model module name(s), if needed." +installperms.args = APP_ARGS + def _start_helper(app_or_project, name, directory, other_name=''): other = {'project': 'app', 'app': 'project'}[app_or_project] if not _is_valid_dir_name(name): diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 506b3668ea..80c422472a 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -190,7 +190,7 @@ class RelatedObject(object): def get_list(self, parent_instance = None): "Get the list of this type of object from an instance of the parent class" if parent_instance != None: - func_name = 'get_%s_list' % self.parent_opts.get_rel_object_method_name(self.opts, self.field) + func_name = 'get_%s_list' % self.get_method_name_part() func = getattr(parent_instance, func_name) list = func() @@ -239,8 +239,9 @@ class RelatedObject(object): return "" % ( self.name, self.field.name) def get_manipulator_fields(self, opts, manipulator, change, follow): + #TODO: remove core fields stuff if change: - meth_name = 'get_%s_count' % self.parent_opts.get_rel_object_method_name(self.opts, self.field) + meth_name = 'get_%s_count' % self.get_method_name_part() count = getattr(manipulator.original_object, meth_name)() count += self.field.rel.num_extra_on_change if self.field.rel.min_num_in_admin: @@ -263,6 +264,9 @@ class RelatedObject(object): def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): return bound_related_object_class(self, field_mapping, original) + def get_method_name_part(self): + return self.parent_opts.get_rel_object_method_name(self.opts, self.field) + class Options: def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='', fields=None, ordering=None, unique_together=None, admin=None, has_related_links=False, @@ -461,7 +465,7 @@ class Options: try: for f in klass._meta.many_to_many: if f.rel and self == f.rel.to: - rel_objs.append((klass._meta, f)) + rel_objs.append(RelatedObject(self, klass._meta, f)) raise StopIteration except StopIteration: continue @@ -856,10 +860,9 @@ class ModelBase(type): old_app._MODELS[i] = new_class # Replace all relationships to the old class with # relationships to the new one. - for related in model._meta.get_all_related_objects(): + for related in model._meta.get_all_related_objects() + \ + model._meta.get_all_related_many_to_many_objects(): related.field.rel.to = opts - for rel_opts, rel_field in model._meta.get_all_related_many_to_many_objects(): - rel_field.rel.to = opts break return new_class @@ -958,7 +961,7 @@ def method_delete(opts, self): self._pre_delete() cursor = db.db.cursor() for related in opts.get_all_related_objects(): - rel_opts_name = opts.get_rel_object_method_name(related.opts, related.field) + rel_opts_name = related.get_method_name_part() if isinstance(related.field.rel, OneToOne): try: sub_obj = getattr(self, 'get_%s' % rel_opts_name)() @@ -969,8 +972,8 @@ def method_delete(opts, self): else: for sub_obj in getattr(self, 'get_%s_list' % rel_opts_name)(): sub_obj.delete() - for rel_opts, rel_field in opts.get_all_related_many_to_many_objects(): - cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (rel_field.get_m2m_db_table(rel_opts), + for related in opts.get_all_related_many_to_many_objects(): + cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (related.field.get_m2m_db_table(related.opts), self._meta.object_name.lower()), [getattr(self, opts.pk.column)]) for f in opts.many_to_many: cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (f.get_m2m_db_table(opts), self._meta.object_name.lower()), @@ -1671,7 +1674,7 @@ def manipulator_save(opts, klass, add, change, self, new_data): if change: if rel_new_data[related.opts.pk.name][0]: try: - old_rel_obj = getattr(self.original_object, 'get_%s' % opts.get_rel_object_method_name(related.opts, related.field))(**{'%s__exact' % related.opts.pk.name: rel_new_data[related.opts.pk.name][0]}) + old_rel_obj = getattr(self.original_object, 'get_%s' % related.get_method_name_part() )(**{'%s__exact' % related.opts.pk.name: rel_new_data[related.opts.pk.name][0]}) except ObjectDoesNotExist: pass diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index a1a33ec4b7..cc95d22cd2 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -706,7 +706,7 @@ class ManyToManyField(Field): def get_manipulator_field_objs(self): if self.rel.raw_id_admin: - return [formfields.CommaSeparatedIntegerField] + return [formfields.RawIdAdminField] else: choices = self.get_choices_default() return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] @@ -741,7 +741,7 @@ class ManyToManyField(Field): instance_ids = [getattr(instance, self.rel.to.pk.column) for instance in get_list_func()] if self.rel.raw_id_admin: new_data[self.name] = ",".join([str(id) for id in instance_ids]) - elif not self.rel.edit_inline: + else: new_data[self.name] = instance_ids else: # In required many-to-many fields with only one available choice, diff --git a/django/core/template/__init__.py b/django/core/template/__init__.py index 3290b5d940..4008958e91 100644 --- a/django/core/template/__init__.py +++ b/django/core/template/__init__.py @@ -395,7 +395,7 @@ class DebugParser(Parser): (command, (origin,line)) = self.command_stack.pop() msg = "Unclosed tag '%s' starting at %s, line %d. Looking for one of: %s " % \ (command, origin, line, ', '.join(parse_until) ) - raise self.error( token.source, msg) + raise self.error( (origin,line), msg) def compile_function_error(self, token, e): if not hasattr(e, 'source'): diff --git a/django/core/template/defaultfilters.py b/django/core/template/defaultfilters.py index 746fa9b873..2604caf7f9 100644 --- a/django/core/template/defaultfilters.py +++ b/django/core/template/defaultfilters.py @@ -24,15 +24,15 @@ def fix_ampersands(value, _): def floatformat(text, _): """ - Displays a floating point number as 34.2 (with one decimal place) - but + Displays a floating point number as 34.2 (with one decimal place) -- but only if there's a point to be displayed """ - from math import modf - if not text: - return '' - if modf(float(text))[0] < 0.1: - return text - return "%.1f" % float(text) + f = float(text) + m = f - int(f) + if m: + return '%.1f' % f + else: + return '%d' % int(f) def linenumbers(value, _): "Displays text with line numbers" @@ -175,7 +175,7 @@ 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) + starttag_re = re.compile(r'<%s(/?>|(\s+[^>]*>))' % tags_re) endtag_re = re.compile('' % tags_re) value = starttag_re.sub('', value) value = endtag_re.sub('', value) diff --git a/django/core/template/defaulttags.py b/django/core/template/defaulttags.py index cc59dad388..7913c154ec 100644 --- a/django/core/template/defaulttags.py +++ b/django/core/template/defaulttags.py @@ -142,6 +142,7 @@ class IfEqualNode(Node): def render(self, context): val1 = resolve_variable(self.var1, context) val2 = resolve_variable(self.var2, context) + if (self.negate and val1 != val2) or (not self.negate and val1 == val2): return self.nodelist_true.render(context) return self.nodelist_false.render(context) diff --git a/django/middleware/template_debug.py b/django/middleware/template_debug.py index f15c5c59f3..222dd5f47d 100644 --- a/django/middleware/template_debug.py +++ b/django/middleware/template_debug.py @@ -14,7 +14,6 @@ class TemplateDebugMiddleware(object): top = max(0, line - context_lines) bottom = min(total, line + 1 + context_lines) - return render_to_response('template_debug', { 'message' : exception.args[0], 'source_lines' : source_lines[top:bottom], diff --git a/django/models/__init__.py b/django/models/__init__.py index c787552b4a..c1f2f95f75 100644 --- a/django/models/__init__.py +++ b/django/models/__init__.py @@ -25,7 +25,7 @@ for mod in modules: # label prepended, and the add_BLAH() method will not be # generated. rel_mod = related.opts.get_model_module() - rel_obj_name = klass._meta.get_rel_object_method_name(related.opts, related.field) + rel_obj_name = related.get_method_name_part() if isinstance(related.field.rel, meta.OneToOne): # Add "get_thingie" methods for one-to-one related objects. # EXAMPLE: Place.get_restaurants_restaurant() @@ -62,18 +62,18 @@ for mod in modules: del rel_obj_name, rel_mod, related # 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, curry(meta.method_get_related_many_to_many, 'get_object', klass._meta, rel_mod, rel_field)) - setattr(klass, 'get_%s_count' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_count', klass._meta, rel_mod, rel_field)) - setattr(klass, 'get_%s_list' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_list', klass._meta, rel_mod, rel_field)) - if rel_opts.app_label == klass._meta.app_label: - func = curry(meta.method_set_related_many_to_many, rel_opts, rel_field) + for related in klass._meta.get_all_related_many_to_many_objects(): + rel_mod = related.opts.get_model_module() + rel_obj_name = related.get_method_name_part() + setattr(klass, 'get_%s' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_object', klass._meta, rel_mod, related.field)) + setattr(klass, 'get_%s_count' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_count', klass._meta, rel_mod, related.field)) + setattr(klass, 'get_%s_list' % rel_obj_name, curry(meta.method_get_related_many_to_many, 'get_list', klass._meta, rel_mod, related.field)) + if related.opts.app_label == klass._meta.app_label: + func = curry(meta.method_set_related_many_to_many, related.opts, related.field) func.alters_data = True - setattr(klass, 'set_%s' % rel_opts.module_name, func) + setattr(klass, 'set_%s' % related.opts.module_name, func) del func - del rel_obj_name, rel_mod, rel_opts, rel_field # clean up + del rel_obj_name, rel_mod, related # clean up # Add "set_thingie_order" and "get_thingie_order" methods for objects # that are ordered with respect to this. diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 039ba68cfb..04e319c1c7 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -37,6 +37,8 @@ def reloader_thread(): mtimes = {} while RUN_RELOADER: for filename in filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values())) + reloadFiles: + if not os.path.exists(filename): + continue # File might be in an egg, so it can't be reloaded. if filename.endswith(".pyc"): filename = filename[:-1] mtime = os.stat(filename).st_mtime diff --git a/django/utils/cache.py b/django/utils/cache.py index fcd0825a22..631ea8f08d 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -21,6 +21,45 @@ import datetime, md5, re from django.conf import settings from django.core.cache import cache +cc_delim_re = re.compile(r'\s*,\s*') +def patch_cache_control(response, **kwargs): + """ + This function patches the Cache-Control header by adding all + keyword arguments to it. The transformation is as follows: + + - all keyword parameter names are turned to lowercase and + all _ will be translated to - + - if the value of a parameter is True (exatly True, not just a + true value), only the parameter name is added to the header + - all other parameters are added with their value, after applying + str to it. + """ + + def dictitem(s): + t = s.split('=',1) + if len(t) > 1: + return (t[0].lower().replace('-', '_'), t[1]) + else: + return (t[0].lower().replace('-', '_'), True) + + def dictvalue(t): + if t[1] == True: + return t[0] + else: + return t[0] + '=' + str(t[1]) + + if response.has_header('Cache-Control'): + print response['Cache-Control'] + cc = cc_delim_re.split(response['Cache-Control']) + print cc + cc = dict([dictitem(el) for el in cc]) + else: + cc = {} + for (k,v) in kwargs.items(): + cc[k.replace('_', '-')] = v + cc = ', '.join([dictvalue(el) for el in cc.items()]) + response['Cache-Control'] = cc + vary_delim_re = re.compile(r',\s*') def patch_response_headers(response, cache_timeout=None): @@ -43,8 +82,7 @@ def patch_response_headers(response, cache_timeout=None): response['Last-Modified'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT') if not response.has_header('Expires'): response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT') - if not response.has_header('Cache-Control'): - response['Cache-Control'] = 'max-age=%d' % cache_timeout + patch_cache_control(response, max_age=cache_timeout) def patch_vary_headers(response, newheaders): """ diff --git a/django/views/decorators/auth.py b/django/views/decorators/auth.py index 8787a8a086..a5a28bb0b2 100644 --- a/django/views/decorators/auth.py +++ b/django/views/decorators/auth.py @@ -4,7 +4,6 @@ def user_passes_test(test_func): redirecting to the log-in page if necessary. The test should be a callable that takes the user object and returns True if the user passes. """ - def _dec(view_func): def _checklogin(request, *args, **kwargs): from django.views.auth.login import redirect_to_login @@ -14,12 +13,10 @@ def user_passes_test(test_func): return _checklogin return _dec - login_required = user_passes_test(lambda u: not u.is_anonymous()) -login_required.__doc__ = ( +login_required.__doc__ = ( """ Decorator for views that checks that the user is logged in, redirecting to the log-in page if necessary. """ ) - diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index 09f9a0139f..f86372cf4e 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -10,8 +10,24 @@ example, as that is unique across a Django project. Additionally, all headers from the response's Vary header will be taken into account on caching -- just like the middleware does. """ +import re from django.utils.decorators import decorator_from_middleware +from django.utils.cache import patch_cache_control from django.middleware.cache import CacheMiddleware cache_page = decorator_from_middleware(CacheMiddleware) + +def cache_control(**kwargs): + + def _cache_controller(viewfunc): + + def _cache_controlled(request, *args, **kw): + response = viewfunc(request, *args, **kw) + patch_cache_control(response, **kwargs) + return response + + return _cache_controlled + + return _cache_controller + diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index 13062b630f..b9b6bac757 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -1,9 +1,35 @@ """ -Decorator for views that supports conditional get on ETag and Last-Modified -headers. +Decorators for views based on HTTP headers. """ from django.utils.decorators import decorator_from_middleware from django.middleware.http import ConditionalGetMiddleware +from django.utils.httpwrappers import HttpResponseForbidden conditional_page = decorator_from_middleware(ConditionalGetMiddleware) + +def require_http_methods(request_method_list): + """ + Decorator to make a view only accept particular request methods. Usage:: + + @require_http_methods(["GET", "POST"]) + def my_view(request): + # I can assume now that only GET or POST requests make it this far + # ... + + Note that request methods ARE case sensitive. + """ + def decorator(func): + def inner(request, *args, **kwargs): + method = request.META.get("REQUEST_METHOD", None) + if method not in request_method_list: + raise HttpResponseForbidden("REQUEST_METHOD '%s' not allowed" % method) + return func(request, *args, **kwargs) + return inner + return decorator + +require_GET = require_http_methods(["GET"]) +require_GET.__doc__ = "Decorator to require that a view only accept the GET method." + +require_POST = require_http_methods(["POST"]) +require_POST.__doc__ = "Decorator to require that a view only accept the POST method." \ No newline at end of file diff --git a/django/views/defaults.py b/django/views/defaults.py index decc220cf7..45e5636c5a 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,10 +1,11 @@ from django.core.exceptions import Http404, ObjectDoesNotExist from django.core.template import Context, loader -from django.models.core import sites +from django.models.core import sites, contenttypes from django.utils import httpwrappers def shortcut(request, content_type_id, object_id): - from django.models.core import contenttypes + """Redirect to an object's page based on a content-type ID and an object ID""" + # Look up the object, making sure it's got a get_absolute_url() function. try: content_type = contenttypes.get_object(pk=content_type_id) obj = content_type.get_object_for_this_type(pk=object_id) @@ -14,25 +15,37 @@ def shortcut(request, content_type_id, object_id): absurl = obj.get_absolute_url() except AttributeError: raise Http404, "%s objects don't have get_absolute_url() methods" % content_type.name + + # Try to figure out the object's domain so we can do a cross-site redirect + # if necessary + + # If the object actually defines a domain, we're done. if absurl.startswith('http://'): return httpwrappers.HttpResponseRedirect(absurl) + object_domain = None + + # Next, look for an many-to-many relationship to sites if hasattr(obj, 'get_site_list'): site_list = obj.get_site_list() if site_list: object_domain = site_list[0].domain + + # Next, look for a many-to-one relationship to sites elif hasattr(obj, 'get_site'): try: object_domain = obj.get_site().domain except sites.SiteDoesNotExist: pass - try: - object_domain = sites.get_current().domain - except sites.SiteDoesNotExist: - pass - if not object_domain: - return httpwrappers.HttpResponseRedirect(absurl) - return httpwrappers.HttpResponseRedirect('http://%s%s' % (object_domain, absurl)) + + # Then, fall back to the current site (if possible) + else: + try: + object_domain = sites.get_current().domain + except sites.SiteDoesNotExist: + # Finally, give up and use a URL without the domain name + return httpwrappers.HttpResponseRedirect(obj.get_absolute_url()) + return httpwrappers.HttpResponseRedirect('http://%s%s' % (object_domain, obj.get_absolute_url())) def page_not_found(request): """ diff --git a/docs/cache.txt b/docs/cache.txt index f15da2660b..c1b1352bca 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -29,7 +29,9 @@ Examples: CACHE_BACKEND Explanation ============================== =========================================== memcached://127.0.0.1:11211/ A memcached backend; the server is running - on localhost port 11211. + on localhost port 11211. You can use + multiple memcached servers by separating + them with semicolons. db://tablename/ A database backend in a table named "tablename". This table should be created @@ -270,6 +272,40 @@ and a list/tuple of header names as its second argument. .. _`HTTP Vary headers`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44 +Controlling cache: Using Vary headers +===================================== + +Another problem with caching is the privacy of data, and the question where data can +be stored in a cascade of caches. A user usually faces two kinds of caches: his own +browser cache (a private cache) and his providers cache (a public cache). A public cache +is used by multiple users and controlled by someone else. This poses problems with private +(in the sense of sensitive) data - you don't want your social security number or your +banking account numbers stored in some public cache. So web applications need a way +to tell the caches what data is private and what is public. + +Other aspects are the definition how long a page should be cached at max, or wether the +cache should allways check for newer versions and only deliver the cache content when +there were no changes (some caches might deliver cached content even if the server page +changed - just because the cache copy isn't yet expired). + +So there are a multitude of options you can control for your pages. This is where the +Cache-Control header (more infos in `HTTP Cache-Control headers`_) comes in. The usage +is quite simple:: + + @cache_control(private=True, must_revalidate=True, max_age=3600) + def my_view(request): + ... + +This would define the view as private, to be revalidated on every access and cache +copies will only be stored for 3600 seconds at max. + +The caching middleware already set's this header up with a max-age of the CACHE_MIDDLEWARE_SETTINGS +setting. And the cache_page decorator does the same. The cache_control decorator correctly merges +different values into one big header, though. But you should take into account that middlewares +might overwrite some of your headers or set their own defaults if you don't give that header yourself. + +.. _`HTTP Cache-Control headers`: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + Other optimizations =================== diff --git a/docs/model-api.txt b/docs/model-api.txt index fa6d8f10e6..3522585e7a 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -750,11 +750,12 @@ Here's a list of all possible ``META`` options. No options are required. Adding ``permissions`` Extra permissions to enter into the permissions table when creating this object. Add, delete and change permissions are automatically created for - each object. This option specifies extra permissions:: + each object that has ``admin`` set. This example specifies an extra + permission, ``can_deliver_pizzas``:: permissions = (("can_deliver_pizzas", "Can deliver pizzas"),) - This is a list of 2-tuples of + This is a list or tuple of 2-tuples in the format ``(permission_code, human_readable_permission_name)``. ``unique_together`` diff --git a/tests/othertests/defaultfilters.py b/tests/othertests/defaultfilters.py new file mode 100644 index 0000000000..d440e25dd5 --- /dev/null +++ b/tests/othertests/defaultfilters.py @@ -0,0 +1,20 @@ +""" +>>> floatformat(7.7, None) +'7.7' +>>> floatformat(7.0, None) +'7' +>>> floatformat(0.7, None) +'0.7' +>>> floatformat(0.07, None) +'0.1' +>>> floatformat(0.007, None) +'0.0' +>>> floatformat(0.0, None) +'0' +""" + +from django.core.template.defaultfilters import * + +if __name__ == '__main__': + import doctest + doctest.testmod()