From 9dda4abee1225db7a7b195b84c915fdd141a7260 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 25 Nov 2005 21:20:09 +0000 Subject: [PATCH] MERGED NEW-ADMIN BRANCH (except for po/mo files, which will come in a separate commit) git-svn-id: http://code.djangoproject.com/svn/django/trunk@1434 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/bin/validate.py | 16 +- django/contrib/admin/filterspecs.py | 152 ++ .../admin/templates/admin/change_form.html | 75 + .../admin/templates/admin/change_list.html | 20 + .../templates/admin/change_list_results.html | 13 + .../admin/templates/admin/date_hierarchy.html | 10 + .../templates/admin/edit_inline_stacked.html | 15 + .../templates/admin/edit_inline_tabular.html | 42 + .../admin/templates/admin/field_line.html | 21 + .../contrib/admin/templates/admin/filter.html | 7 + .../admin/templates/admin/filters.html | 5 + .../admin/templates/admin/pagination.html | 9 + .../admin/templates/admin/search_form.html | 14 + .../admin/templates/admin/submit_line.html | 8 + .../templates/admin_doc/bookmarklets.html | 22 +- .../admin/templates/widget/date_time.html | 4 + .../admin/templates/widget/default.html | 1 + .../contrib/admin/templates/widget/file.html | 4 + .../admin/templates/widget/foreign.html | 7 + .../admin/templates/widget/many_to_many.html | 1 + .../contrib/admin/templatetags/admin_list.py | 278 ++++ .../admin/templatetags/admin_modify.py | 258 ++++ django/contrib/admin/views/main.py | 1285 ++++++----------- django/core/formfields.py | 154 +- django/core/management.py | 6 +- django/core/meta/__init__.py | 442 ++++-- django/core/meta/fields.py | 213 ++- django/models/__init__.py | 55 +- django/views/generic/create_update.py | 17 +- setup.py | 1 + 30 files changed, 2046 insertions(+), 1109 deletions(-) create mode 100644 django/contrib/admin/filterspecs.py create mode 100644 django/contrib/admin/templates/admin/change_form.html create mode 100644 django/contrib/admin/templates/admin/change_list.html create mode 100644 django/contrib/admin/templates/admin/change_list_results.html create mode 100644 django/contrib/admin/templates/admin/date_hierarchy.html create mode 100644 django/contrib/admin/templates/admin/edit_inline_stacked.html create mode 100644 django/contrib/admin/templates/admin/edit_inline_tabular.html create mode 100644 django/contrib/admin/templates/admin/field_line.html create mode 100644 django/contrib/admin/templates/admin/filter.html create mode 100644 django/contrib/admin/templates/admin/filters.html create mode 100644 django/contrib/admin/templates/admin/pagination.html create mode 100644 django/contrib/admin/templates/admin/search_form.html create mode 100644 django/contrib/admin/templates/admin/submit_line.html create mode 100644 django/contrib/admin/templates/widget/date_time.html create mode 100644 django/contrib/admin/templates/widget/default.html create mode 100644 django/contrib/admin/templates/widget/file.html create mode 100644 django/contrib/admin/templates/widget/foreign.html create mode 100644 django/contrib/admin/templates/widget/many_to_many.html create mode 100644 django/contrib/admin/templatetags/admin_list.py create mode 100644 django/contrib/admin/templatetags/admin_modify.py diff --git a/django/bin/validate.py b/django/bin/validate.py index 220c173edc..9e59fe7823 100644 --- a/django/bin/validate.py +++ b/django/bin/validate.py @@ -16,19 +16,19 @@ def validate_class(klass): 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, \ + for related in opts.get_followed_related_objects(): + assert len([f for f in related.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) + (related.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, \ + for related in opts.get_all_related_objects(): + if related.opts in related_apps_seen: + assert related.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) + (related.opts.object_name, related.field.name, opts.object_name, rel_opts.object_name) + related_apps_seen.append(related.opts) # Etc. if opts.admin is not None: assert opts.admin.ordering or opts.ordering, \ diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py new file mode 100644 index 0000000000..31a6ba37c9 --- /dev/null +++ b/django/contrib/admin/filterspecs.py @@ -0,0 +1,152 @@ +""" +FilterSpec encapsulates the logic for displaying filters in the Django admin. +Filters are specified in models with the "list_filter" option. + +Each filter subclass knows how to display a filter for a field that passes a +certain test -- e.g. being a DateField or ForeignKey. +""" + +from django.core import meta +import datetime + +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 choices(self, cl): + raise NotImplementedError() + + def title(self): + return self.field.verbose_name + + def output(self, cl): + t = [] + if self.has_output(): + t.append(_('

By %s:

\n\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 title(self): + return self.lookup_title + + def choices(self, cl): + yield {'selected': self.lookup_val is None, + 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), + 'display': _('All')} + for val in self.lookup_choices: + pk_val = getattr(val, self.field.rel.to.pk.attname) + yield {'selected': self.lookup_val == str(pk_val), + 'query_string': cl.get_query_string( {self.lookup_kwarg: pk_val}), + 'display': val} + +FilterSpec.register(lambda f: bool(f.rel), RelatedFilterSpec) + +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(self.lookup_kwarg, None) + + def choices(self, cl): + yield {'selected': self.lookup_val is None, + 'query_string': cl.get_query_string( {}, [self.lookup_kwarg]), + 'display': _('All')} + for k, v in self.field.choices: + yield {'selected': str(k) == self.lookup_val, + 'query_string': cl.get_query_string( {self.lookup_kwarg: k}), + 'display': v} + +FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec) + +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(self.field_generic)]) + + today = datetime.date.today() + one_week_ago = today - datetime.timedelta(days=7) + today_str = isinstance(self.field, meta.DateTimeField) and today.strftime('%Y-%m-%d 23:59:59') or today.strftime('%Y-%m-%d') + + 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 title(self): + return self.field.verbose_name + + def choices(self, cl): + for title, param_dict in self.links: + yield {'selected': self.date_params == param_dict, + 'query_string': cl.get_query_string( param_dict, self.field_generic), + 'display': title} + +FilterSpec.register(lambda f: isinstance(f, meta.DateField), DateFieldFilterSpec) + +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 title(self): + return self.field.verbose_name + + def choices(self, cl): + for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')): + yield {'selected': self.lookup_val == v and not self.lookup_val2, + 'query_string': cl.get_query_string( {self.lookup_kwarg: v}, [self.lookup_kwarg2]), + 'display': k} + if isinstance(self.field, meta.NullBooleanField): + yield {'selected': self.lookup_val2 == 'True', + 'query_string': cl.get_query_string( {self.lookup_kwarg2: 'True'}, [self.lookup_kwarg]), + 'display': _('Unknown')} + +FilterSpec.register(lambda f: isinstance(f, meta.BooleanField) or isinstance(f, meta.NullBooleanField), BooleanFieldFilterSpec) diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html new file mode 100644 index 0000000000..b5ae024866 --- /dev/null +++ b/django/contrib/admin/templates/admin/change_form.html @@ -0,0 +1,75 @@ +{% extends "admin/base_site" %} +{% load i18n %} +{% load admin_modify %} +{% load adminmedia %} +{% block extrahead %} +{% for js in bound_manipulator.javascript_imports %}{% include_admin_script js %}{% endfor %} +{% endblock %} +{% block coltype %}{{ bound_manipulator.coltype }}{% endblock %} +{% block bodyclass %}{{ app_label }}-{{ bound_manipulator.object_name.lower }} change-form{% endblock %} +{% block breadcrumbs %}{% if not is_popup %} + +{% endif %}{% endblock %} +{% block content %}
+{% if change %}{% if not is_popup %} + +{% endif %}{% endif %} +
{% block form_top %}{% endblock %} +{% if is_popup %}{% endif %} +{% if bound_manipulator.save_on_top %}{% submit_row bound_manipulator %}{% endif %} +{% if form.error_dict %} +

+ {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} +

+{% endif %} +{% 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 %} + {% admin_field_line bound_field_line %} + {% for bound_field in bound_field_line %} + {% filter_interface_script_maybe bound_field %} + {% endfor %} + {% endfor %} +
+{% endfor %} +{% block after_field_sets %}{% endblock %} +{% if change %} + {% if bound_manipulator.ordered_objects %} +

{% trans "Ordering" %}

+
+ {% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %} +

{{ form.order_ }}

+
+ {% endif %} +{% endif %} +{% for related_object in bound_manipulator.inline_related_objects %}{% edit_inline related_object %}{% endfor %} +{% block after_related_objects %}{% endblock %} +{% submit_row bound_manipulator %} +{% if add %} + +{% endif %} +{% if bound_manipulator.auto_populated_fields %} + +{% endif %} +{% if change %} + {% if bound_manipulator.ordered_objects %} + {% if form.order_objects %}
    + {% for object in form.order_objects %} +
  • + {{ object|truncatewords:"5" }} +
  • + {% endfor %} +
{% endif %} + {% endif %} +{% endif %} +
+{% endblock %} 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..ccc3990519 --- /dev/null +++ b/django/contrib/admin/templates/admin/change_list.html @@ -0,0 +1,20 @@ +{% load admin_list %} +{% load i18n %} +{% 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 %} + +{% endif %} +
+{% search_form cl %} +{% date_hierarchy cl %} +{% filters cl %} +{% result_list cl %} +{% pagination cl %} +
+
+{% endblock %} diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html new file mode 100644 index 0000000000..92e06c5fff --- /dev/null +++ b/django/contrib/admin/templates/admin/change_list_results.html @@ -0,0 +1,13 @@ + + + +{% for header in result_headers %} +{% if header.sortable %}{% endif %} +{{ header.text|capfirst }} +{% if header.sortable %}{% endif %}{% endfor %} + + +{% for result in results %} +{% for item in result %}{{ item }}{% endfor %} +{% endfor %} +
diff --git a/django/contrib/admin/templates/admin/date_hierarchy.html b/django/contrib/admin/templates/admin/date_hierarchy.html new file mode 100644 index 0000000000..a53d810f93 --- /dev/null +++ b/django/contrib/admin/templates/admin/date_hierarchy.html @@ -0,0 +1,10 @@ +{% if show %} +
+
+
+{% endif %} \ No newline at end of file diff --git a/django/contrib/admin/templates/admin/edit_inline_stacked.html b/django/contrib/admin/templates/admin/edit_inline_stacked.html new file mode 100644 index 0000000000..5e5ea8c0fc --- /dev/null +++ b/django/contrib/admin/templates/admin/edit_inline_stacked.html @@ -0,0 +1,15 @@ +
+ {% for fcw in bound_related_object.form_field_collection_wrappers %} +

{{ bound_related_object.relation.opts.verbose_name|capfirst }} #{{ forloop.counter }}

+ {% if bound_related_object.show_url %}{% if fcw.obj.original %} +

View on site

+ {% endif %}{% endif %} + {% for bound_field in fcw.bound_fields %} + {% if bound_field.hidden %} + {% field_widget bound_field %} + {% else %} + {% admin_field_line bound_field %} + {% endif %} + {% endfor %} + {% endfor %} +
\ No newline at end of file diff --git a/django/contrib/admin/templates/admin/edit_inline_tabular.html b/django/contrib/admin/templates/admin/edit_inline_tabular.html new file mode 100644 index 0000000000..c06ee05df8 --- /dev/null +++ b/django/contrib/admin/templates/admin/edit_inline_tabular.html @@ -0,0 +1,42 @@ +
+

{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}

+ + {% for fw in bound_related_object.field_wrapper_list %} + {% if fw.needs_header %} + {{ fw.field.verbose_name|capfirst }} + {% endif %} + {% endfor %} + {% for fcw in bound_related_object.form_field_collection_wrappers %} + {% if change %}{% if original_row_needed %} + {% if fcw.obj.original %} + + {% endif %} + {% endif %}{% endif %} + {% if fcw.obj.errors %} + + {% endif %} + + {% for bound_field in fcw.bound_fields %} + {% if not bound_field.hidden %} + + {% endif %} + {% endfor %} + {% if bound_related_object.show_url %}{% endif %} + + + {% endfor %}
{{ fcw.obj.original }}
+ {{ fcw.obj.html_combined_error_list }} +
+ {% field_widget bound_field %} + + {% if fcw.obj.original %}View on site{% endif %} +
+ + {% for fcw in bound_related_object.form_field_collection_wrappers %} + {% for bound_field in fcw.bound_fields %} + {% if bound_field.hidden %} + {% field_widget bound_field %} + {% endif %} + {% endfor %} + {% endfor %} +
diff --git a/django/contrib/admin/templates/admin/field_line.html b/django/contrib/admin/templates/admin/field_line.html new file mode 100644 index 0000000000..5e526e6fd6 --- /dev/null +++ b/django/contrib/admin/templates/admin/field_line.html @@ -0,0 +1,21 @@ +
+{% for bound_field in bound_fields %}{{ bound_field.html_error_list }}{% endfor %} +{% for bound_field in bound_fields %} + {% if bound_field.has_label_first %} + {% field_label bound_field %} + {% endif %} + {% field_widget bound_field %} + {% if not bound_field.has_label_first %} + {% field_label bound_field %} + {% endif %} + {% if change %} + {% if bound_field.field.primary_key %} + {{ bound_field.original_value }} + {% endif %} + {% if bound_field.raw_id_admin %} + {% if bound_field.existing_display %} {{ bound_field.existing_display|truncatewords:"14" }}{% endif %} + {% endif %} + {% endif %} + {% if bound_field.field.help_text %}

{{ bound_field.field.help_text }}

{% endif %} +{% endfor %} +
diff --git a/django/contrib/admin/templates/admin/filter.html b/django/contrib/admin/templates/admin/filter.html new file mode 100644 index 0000000000..385b1824f2 --- /dev/null +++ b/django/contrib/admin/templates/admin/filter.html @@ -0,0 +1,7 @@ +

{% blocktrans %} By {{ title }} {% endblocktrans %}

+ diff --git a/django/contrib/admin/templates/admin/filters.html b/django/contrib/admin/templates/admin/filters.html new file mode 100644 index 0000000000..b3c6a25831 --- /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 %} + {% filter cl spec %} +{% endfor %}
{% endif %} diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html new file mode 100644 index 0000000000..5d80ecdcc4 --- /dev/null +++ b/django/contrib/admin/templates/admin/pagination.html @@ -0,0 +1,9 @@ +

+{% 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 show_all_url %}  Show all{% endif %} +

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..19b6b7eb95 --- /dev/null +++ b/django/contrib/admin/templates/admin/search_form.html @@ -0,0 +1,14 @@ +{% if cl.lookup_opts.admin.search_fields %} +
+ +{% endif %} diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html new file mode 100644 index 0000000000..25f581963e --- /dev/null +++ b/django/contrib/admin/templates/admin/submit_line.html @@ -0,0 +1,8 @@ +{% load i18n %} +
+{% if show_delete_link %}

{% trans "Delete" %}

{% endif %} +{% if show_save_as_new %}{%endif%} +{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_continue %}{% endif %} +{% if show_save %}{% endif %} +
diff --git a/django/contrib/admin/templates/admin_doc/bookmarklets.html b/django/contrib/admin/templates/admin_doc/bookmarklets.html index d396ec53b5..c069597219 100644 --- a/django/contrib/admin/templates/admin_doc/bookmarklets.html +++ b/django/contrib/admin/templates/admin_doc/bookmarklets.html @@ -1,30 +1,32 @@ {% extends "admin/base_site" %} -{% block breadcrumbs %}{% endblock %} +{% block breadcrumbs %}{% endblock %} -{% block title %}Documentation bookmarklets{% endblock %} +{% block title %}{% trans "Documentation bookmarklets" %}{% endblock %} {% block content %} +{% blocktrans %}

To install bookmarklets, drag the link to your bookmarks toolbar, or right-click the link and add it to your bookmarks. Now you can select the bookmarklet from any page in the site. Note that some of these bookmarklets require you to be viewing the site from a computer designated as "internal" (talk to your system administrator if you aren't sure if your computer is "internal").

+{% endblocktrans %}
-

Documentation for this page

-

Jumps you from any page to the documentation for the view that generates that page.

+

{% trans "Documentation for this page" %}

+

{% trans "Jumps you from any page to the documentation for the view that generates that page." %}

-

Show object ID

-

Shows the content-type and unique ID for pages that represent a single object.

+

{% trans "Show object ID" %}

+

{% trans "Shows the content-type and unique ID for pages that represent a single object." %}

-

Edit this object (current window)

-

Jumps to the admin page for pages that represent a single object.

+

{% trans "Edit this object (current window)" %}

+

{% trans "Jumps to the admin page for pages that represent a single object." %}

-

Edit this object (new window)

-

As above, but opens the admin page in a new window.

+

{% trans "Edit this object (new window)" %}

+

{% trans "As above, but opens the admin page in a new window." %}

{% endblock %} diff --git a/django/contrib/admin/templates/widget/date_time.html b/django/contrib/admin/templates/widget/date_time.html new file mode 100644 index 0000000000..2d5126c5bb --- /dev/null +++ b/django/contrib/admin/templates/widget/date_time.html @@ -0,0 +1,4 @@ +

+ Date: {{ bound_field.form_fields.0 }}
+ Time: {{ bound_field.form_fields.1 }} +

diff --git a/django/contrib/admin/templates/widget/default.html b/django/contrib/admin/templates/widget/default.html new file mode 100644 index 0000000000..08a82fdbfa --- /dev/null +++ b/django/contrib/admin/templates/widget/default.html @@ -0,0 +1 @@ +{% output_all bound_field.form_fields %} diff --git a/django/contrib/admin/templates/widget/file.html b/django/contrib/admin/templates/widget/file.html new file mode 100644 index 0000000000..f81534b474 --- /dev/null +++ b/django/contrib/admin/templates/widget/file.html @@ -0,0 +1,4 @@ +{% if bound_field.original_value %} +Currently: {{ bound_field.original_value }}
+Change: {% output_all bound_field.form_fields %} +{% else %} {% output_all bound_field.form_fields %} {% endif %} diff --git a/django/contrib/admin/templates/widget/foreign.html b/django/contrib/admin/templates/widget/foreign.html new file mode 100644 index 0000000000..9decb1143c --- /dev/null +++ b/django/contrib/admin/templates/widget/foreign.html @@ -0,0 +1,7 @@ +{% output_all bound_field.form_fields %} +{% if bound_field.raw_id_admin %} + Lookup +{% else %} +{% if bound_field.needs_add_label %} + Add Another +{% endif %} {% endif %} diff --git a/django/contrib/admin/templates/widget/many_to_many.html b/django/contrib/admin/templates/widget/many_to_many.html new file mode 100644 index 0000000000..151fe04f30 --- /dev/null +++ b/django/contrib/admin/templates/widget/many_to_many.html @@ -0,0 +1 @@ +{% include "widget/foreign" %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py new file mode 100644 index 0000000000..9733114872 --- /dev/null +++ b/django/contrib/admin/templatetags/admin_list.py @@ -0,0 +1,278 @@ +from django.contrib.admin.views.main import MAX_SHOW_ALL_ALLOWED, DEFAULT_RESULTS_PER_PAGE, ALL_VAR +from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR +from django.contrib.admin.views.main import IS_POPUP_VAR, EMPTY_CHANGELIST_VALUE, MONTHS +from django.core import meta, template +from django.core.exceptions import ObjectDoesNotExist +from django.core.template.decorators import simple_tag, inclusion_tag +from django.utils import dateformat +from django.utils.html import strip_tags, escape +from django.utils.text import capfirst +from django.utils.translation import get_date_formats +from django.conf.settings import ADMIN_MEDIA_PREFIX + +DOT = '.' + +#@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 == cl.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)) + + need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page + return { + 'cl': cl, + 'pagination_required': pagination_required, + 'show_all_url': need_show_all_link and cl.get_query_string({ALL_VAR: ''}), + 'page_range': page_range, + 'ALL_VAR': ALL_VAR, + '1': 1, + } +pagination = inclusion_tag('admin/pagination')(pagination) + +def result_headers(cl): + lookup_opts = cl.lookup_opts + + 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(cl.mod.Klass, field_name) # Let AttributeErrors propogate. + try: + header = func.short_description + except AttributeError: + header = func.__name__.replace('_', ' ') + # Non-field list_display values don't get ordering capability. + yield {"text": header} + else: + if isinstance(f.rel, meta.ManyToOne) and f.null: + yield {"text": f.verbose_name} + else: + th_classes = [] + new_order_type = 'asc' + if field_name == cl.order_field: + th_classes.append('sorted %sending' % cl.order_type.lower()) + new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()] + + yield {"text": f.verbose_name, + "sortable": True, + "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}), + "class_attrib": (th_classes and ' class="%s"' % ' '.join(th_classes) or '')} + +def items_for_result(cl, result): + first = True + pk = cl.lookup_opts.pk.attname + for field_name in cl.lookup_opts.admin.list_display: + row_class = '' + try: + f = cl.lookup_opts.get_field(field_name) + except meta.FieldDoesNotExist: + # For non-field list_display values, the value is a method + # name. Execute the method. + try: + func = getattr(result, field_name) + result_repr = str(func()) + except AttributeError, ObjectDoesNotExist: + result_repr = EMPTY_CHANGELIST_VALUE + else: + # Strip HTML tags in the resulting text, except if the + # function has an "allow_tags" attribute set to True. + if not getattr(func, 'allow_tags', False): + result_repr = strip_tags(result_repr) + else: + field_val = getattr(result, f.attname) + + 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 and times are special: They're formatted in a certain way. + elif isinstance(f, meta.DateField) or isinstance(f, meta.TimeField): + if field_val: + (date_format, datetime_format, time_format) = get_date_formats() + if isinstance(f, meta.DateTimeField): + result_repr = capfirst(dateformat.format(field_val, datetime_format)) + elif isinstance(f, meta.TimeField): + result_repr = capfirst(dateformat.time_format(field_val, time_format)) + else: + result_repr = capfirst(dateformat.format(field_val, date_format)) + 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)) + if result_repr == '': + result_repr = ' ' + if first: # First column is a special case + first = False + url = cl.url_for_result(result) + result_id = getattr(result, pk) + yield ('%s' % \ + (row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %r); return false;"' % result_id or ''), result_repr)) + else: + yield ('%s' % (row_class, result_repr)) + +def results(cl): + for res in cl.result_list: + yield list(items_for_result(cl,res)) + +#@inclusion_tag("admin/change_list_results") +def result_list(cl): + res = list(results(cl)) + return {'cl': cl, + 'result_headers': list(result_headers(cl)), + 'results': list(results(cl))} +result_list = inclusion_tag("admin/change_list_results")(result_list) + +#@inclusion_tag("admin/date_hierarchy") +def date_hierarchy(cl): + lookup_opts, params, lookup_params, lookup_mod = \ + cl.lookup_opts, cl.params, cl.lookup_params, cl.lookup_mod + + if lookup_opts.admin.date_hierarchy: + field_name = lookup_opts.admin.date_hierarchy + + year_field = '%s__year' % field_name + month_field = '%s__month' % field_name + day_field = '%s__day' % field_name + field_generic = '%s__' % field_name + year_lookup = params.get(year_field) + month_lookup = params.get(month_field) + day_lookup = params.get(day_field) + + def link(d): + return cl.get_query_string(d, [field_generic]) + + def get_dates(unit, params): + return getattr(lookup_mod, 'get_%s_list' % field_name)(unit, **params) + + if year_lookup and month_lookup and day_lookup: + month_name = MONTHS[int(month_lookup)] + return { + 'show': True, + 'back': { + 'link': link({year_field: year_lookup, month_field: month_lookup}), + 'title': "%s %s" % (month_name, year_lookup) + }, + 'choices': [{'title': "%s %s" % (month_name, day_lookup)}] + } + elif year_lookup and month_lookup: + date_lookup_params = lookup_params.copy() + date_lookup_params.update({year_field: year_lookup, month_field: month_lookup}) + days = get_dates('day', date_lookup_params) + return { + 'show': True, + 'back': { + 'link': link({year_field: year_lookup}), + 'title': year_lookup + }, + 'choices': [{ + 'link': link({year_field: year_lookup, month_field: month_lookup, day_field: day.day}), + 'title': day.strftime('%B %d') + } for day in days] + } + elif year_lookup: + date_lookup_params = lookup_params.copy() + date_lookup_params.update({year_field: year_lookup}) + months = get_dates('month', date_lookup_params) + return { + 'show' : True, + 'back': { + 'link' : link({}), + 'title': _('All dates') + }, + 'choices': [{ + 'link': link( {year_field: year_lookup, month_field: month.month}), + 'title': "%s %s" % (month.strftime('%B') , month.year) + } for month in months] + } + else: + years = get_dates('year', lookup_params) + return { + 'show': True, + 'choices': [{ + 'link': link({year_field: year.year}), + 'title': year.year + } for year in years ] + } +date_hierarchy = inclusion_tag('admin/date_hierarchy')(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) + +#@inclusion_tag('admin/filter') +def filter(cl, spec): + return {'title': spec.title(), 'choices' : list(spec.choices(cl))} +filter = inclusion_tag('admin/filter')(filter) + +#@inclusion_tag('admin/filters') +def filters(cl): + return {'cl': cl} +filters = inclusion_tag('admin/filters')(filters) diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py new file mode 100644 index 0000000000..891aec1b1f --- /dev/null +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -0,0 +1,258 @@ +from django.core import template, template_loader, meta +from django.utils.html import escape +from django.utils.text import capfirst +from django.utils.functional import curry +from django.core.template.decorators import simple_tag, inclusion_tag +from django.contrib.admin.views.main import AdminBoundField +from django.core.meta.fields import BoundField, Field +from django.core.meta import BoundRelatedObject, TABULAR, STACKED +from django.conf.settings import ADMIN_MEDIA_PREFIX +import re + +word_re = re.compile('[A-Z][a-z]+') + +def class_name_to_underscored(name): + return '_'.join([s.lower() for s in word_re.findall(name)[:-1]]) + +#@simple_tag +def include_admin_script(script_path): + return '' % (ADMIN_MEDIA_PREFIX, script_path) +include_admin_script = simple_tag(include_admin_script) + +#@inclusion_tag('admin/submit_line', takes_context=True) +def submit_row(context, bound_manipulator): + change = context['change'] + add = context['add'] + show_delete = context['show_delete'] + has_delete_permission = context['has_delete_permission'] + is_popup = context['is_popup'] + return { + 'onclick_attrib': (bound_manipulator.ordered_objects and change + and 'onclick="submitOrderForm();"' or ''), + 'show_delete_link': (not is_popup and has_delete_permission + and (change or show_delete)), + 'show_save_as_new': not is_popup and change and bound_manipulator.save_as, + 'show_save_and_add_another': not is_popup and (not bound_manipulator.save_as or add), + 'show_save_and_continue': not is_popup, + 'show_save': True + } +submit_row = inclusion_tag('admin/submit_line', takes_context=True)(submit_row) + +#@simple_tag +def field_label(bound_field): + class_names = [] + if isinstance(bound_field.field, meta.BooleanField): + class_names.append("vCheckboxLabel") + colon = "" + else: + if not bound_field.field.blank: + class_names.append('required') + if not bound_field.first: + class_names.append('inline') + colon = ":" + class_str = class_names and ' class="%s"' % ' '.join(class_names) or '' + return ' ' % (bound_field.element_id, class_str, \ + capfirst(bound_field.field.verbose_name), colon) +field_label = simple_tag(field_label) + +class FieldWidgetNode(template.Node): + nodelists = {} + default = None + + def __init__(self, bound_field_var): + self.bound_field_var = bound_field_var + + def get_nodelist(cls, klass): + if not cls.nodelists.has_key(klass): + try: + field_class_name = klass.__name__ + template_name = "widget/%s" % \ + class_name_to_underscored(field_class_name) + nodelist = template_loader.get_template(template_name).nodelist + except template.TemplateDoesNotExist: + super_klass = bool(klass.__bases__) and klass.__bases__[0] or None + if super_klass and super_klass != Field: + nodelist = cls.get_nodelist(super_klass) + else: + if not cls.default: + cls.default = template_loader.get_template("widget/default").nodelist + nodelist = cls.default + + cls.nodelists[klass] = nodelist + return nodelist + else: + return cls.nodelists[klass] + get_nodelist = classmethod(get_nodelist) + + def render(self, context): + bound_field = template.resolve_variable(self.bound_field_var, context) + + context.push() + context['bound_field'] = bound_field + + output = self.get_nodelist(bound_field.field.__class__).render(context) + context.pop() + return output + +class FieldWrapper(object): + def __init__(self, field ): + self.field = field + + def needs_header(self): + return not isinstance(self.field, meta.AutoField) + + def header_class_attribute(self): + return self.field.blank and ' class="optional"' or '' + + def use_raw_id_admin(self): + return isinstance(self.field.rel, (meta.ManyToOne, meta.ManyToMany)) \ + and self.field.rel.raw_id_admin + +class FormFieldCollectionWrapper(object): + def __init__(self, field_mapping, fields): + self.field_mapping = field_mapping + self.fields = fields + self.bound_fields = [AdminBoundField(field, self.field_mapping, field_mapping['original']) + for field in self.fields] + +class TabularBoundRelatedObject(BoundRelatedObject): + def __init__(self, related_object, field_mapping, original): + super(TabularBoundRelatedObject, self).__init__(related_object, field_mapping, original) + self.field_wrapper_list = [FieldWrapper(field) for field in self.relation.editable_fields()] + + fields = self.relation.editable_fields() + + self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping, fields) + for field_mapping in self.field_mappings] + self.original_row_needed = max([fw.use_raw_id_admin() for fw in self.field_wrapper_list]) + self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url') + + def template_name(self): + return "admin/edit_inline_tabular" + +class StackedBoundRelatedObject(BoundRelatedObject): + def __init__(self, related_object, field_mapping, original): + super(StackedBoundRelatedObject, self).__init__(related_object, field_mapping, original) + fields = self.relation.editable_fields() + self.form_field_collection_wrappers = [FormFieldCollectionWrapper(field_mapping ,fields) + for field_mapping in self.field_mappings] + self.show_url = original and hasattr(self.relation.opts, 'get_absolute_url') + + def template_name(self): + return "admin/edit_inline_stacked" + +bound_related_object_overrides = { + TABULAR: TabularBoundRelatedObject, + STACKED: StackedBoundRelatedObject, +} + +class EditInlineNode(template.Node): + def __init__(self, rel_var): + self.rel_var = rel_var + + def render(self, context): + relation = template.resolve_variable(self.rel_var, context) + + context.push() + + klass = relation.field.rel.edit_inline + bound_related_object_class = bound_related_object_overrides.get(klass, klass) + + original = context.get('original', None) + + bound_related_object = relation.bind(context['form'], original, bound_related_object_class) + context['bound_related_object'] = bound_related_object + + t = template_loader.get_template(bound_related_object.template_name()) + + output = t.render(context) + + context.pop() + return output + +#@simple_tag +def output_all(form_fields): + return ''.join([str(f) for f in form_fields]) +output_all = simple_tag(output_all) + +#@simple_tag +def auto_populated_field_script(auto_pop_fields, change = False): + for field in auto_pop_fields: + t = [] + if change: + t.append('document.getElementById("id_%s")._changed = true;' % field.name) + else: + t.append('document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name) + + add_values = ' + " " + '.join(['document.getElementById("id_%s").value' % g for g in field.prepopulate_from]) + for f in field.prepopulate_from: + t.append('document.getElementById("id_%s").onkeyup = function() {' \ + ' var e = document.getElementById("id_%s");' \ + ' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % ( + f, field.name, add_values, field.maxlength)) + return ''.join(t) +auto_populated_field_script = simple_tag(auto_populated_field_script) + +#@simple_tag +def filter_interface_script_maybe(bound_field): + f = bound_field.field + if f.rel and isinstance(f.rel, meta.ManyToMany) and f.rel.filter_interface: + return '\n' % ( + f.name, f.verbose_name, f.rel.filter_interface-1, ADMIN_MEDIA_PREFIX) + else: + return '' +filter_interface_script_maybe = simple_tag(filter_interface_script_maybe) + +def do_one_arg_tag(node_factory, parser,token): + tokens = token.contents.split() + if len(tokens) != 2: + raise template.TemplateSyntaxError("%s takes 1 argument" % tokens[0]) + return node_factory(tokens[1]) + +def register_one_arg_tag(node): + tag_name = class_name_to_underscored(node.__name__) + parse_func = curry(do_one_arg_tag, node) + template.register_tag(tag_name, parse_func) + +one_arg_tag_nodes = ( + FieldWidgetNode, + EditInlineNode, +) + +for node in one_arg_tag_nodes: + register_one_arg_tag(node) + +#@inclusion_tag('admin/field_line', takes_context=True) +def admin_field_line(context, argument_val): + if (isinstance(argument_val, BoundField)): + bound_fields = [argument_val] + else: + bound_fields = [bf for bf in argument_val] + add = context['add'] + change = context['change'] + + class_names = ['form-row'] + for bound_field in bound_fields: + for f in bound_field.form_fields: + if f.errors(): + class_names.append('errors') + break + + # Assumes BooleanFields won't be stacked next to each other! + if isinstance(bound_fields[0].field, meta.BooleanField): + class_names.append('checkbox-row') + + return { + 'add': context['add'], + 'change': context['change'], + 'bound_fields': bound_fields, + 'class_names': " ".join(class_names), + } +admin_field_line = inclusion_tag('admin/field_line', takes_context=True)(admin_field_line) + +#@simple_tag +def object_pk(bound_manip, ordered_obj): + return bound_manip.get_ordered_object_pk(ordered_obj) + +object_pk = simple_tag(object_pk) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 9c2927000a..f62fbbc01d 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,19 +1,36 @@ -# Generic admin views, with admin templates created dynamically at runtime. - +# Generic admin views. from django.contrib.admin.views.decorators import staff_member_required -from django.core import formfields, meta +from django.contrib.admin.filterspecs import FilterSpec +from django.core import formfields, meta, template from django.core.template import loader +from django.core.meta.fields import BoundField, BoundFieldLine, BoundFieldSet from django.core.exceptions import Http404, ObjectDoesNotExist, PermissionDenied from django.core.extensions import DjangoContext as Context from django.core.extensions import get_object_or_404, render_to_response +from django.core.paginator import ObjectPaginator, InvalidPage +from django.conf.settings import ADMIN_MEDIA_PREFIX from django.models.admin import log 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.utils.translation import get_date_formats +from django.utils import dateformat +from django.utils.dates import MONTHS +from django.utils.html import escape import operator +# The system will display a "Show all" link only if the total result count +# is less than or equal to this setting. +MAX_SHOW_ALL_ALLOWED = 200 + +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,747 +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)) + 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 ChangeList(object): + def __init__(self, request, app_label, module_name): + self.get_modules_and_options(app_label, module_name, request) + self.get_search_parameters(request) + self.get_ordering() + self.query = request.GET.get(SEARCH_VAR,'') + self.get_lookup_params() + self.get_results(request) + self.title = (self.is_popup + and _('Select %s') % self.opts.verbose_name + or _('Select %s to change') % self.opts.verbose_name) + self.get_filters(request) + self.pk_attname = self.lookup_opts.pk.attname - DEFAULT_RESULTS_PER_PAGE = 100 + 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) - ALL_VAR = 'all' - ORDER_VAR = 'o' - ORDER_TYPE_VAR = 'ot' - PAGE_VAR = 'p' - SEARCH_VAR = 'q' - IS_POPUP_VAR = 'pop' + 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') - mod, opts = _get_mod_opts(app_label, module_name) - if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): - raise PermissionDenied + 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 - lookup_mod, lookup_opts = mod, opts + self.lookup_mod, self.lookup_opts = self.mod, self.opts - # 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: - 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, '') + 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] - # 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) - if lookup_opts.admin.list_select_related: - lookup_params['select_related'] = True - else: - # Use select_related if one of the list_display options is a field with - # a relationship. - for field_name in lookup_opts.admin.list_display: - try: - f = lookup_opts.get_field(field_name) - except meta.FieldDoesNotExist: - pass - else: - if isinstance(f.rel, meta.ManyToOne): - lookup_params['select_related'] = True - break - lookup_params['order_by'] = ((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: + 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: - result_list = p.get_page(page_num) - except paginator.InvalidPage: - result_list = [] + 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() - # 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 - 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.attname) - 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') + 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 - # 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): + # 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(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 + result_list = paginator.get_page(page_num) + except InvalidPage: + result_list = [] + (self.result_count, self.full_result_count, self.result_list, + self.can_show_all, self.multi_page, self.paginator) = (result_count, + full_result_count, result_list, can_show_all, multi_page, paginator ) + + def url_for_result(self, result): + return "%s/" % getattr(result, self.pk_attname) + + def get_ordering(self): + lookup_opts, params = self.lookup_opts, self.params + # For ordering, first check the "ordering" parameter in the admin options, + # then check the object's default ordering. If neither of those exist, + # order descending by ID by default. Finally, look for manually-specified + # ordering from the query string. + ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name] + + # Normalize it to new-style ordering. + ordering = meta.handle_legacy_orderlist(ordering) + + if ordering[0].startswith('-'): + order_field, order_type = ordering[0][1:], 'desc' + else: + order_field, order_type = ordering[0], 'asc' + if params.has_key(ORDER_VAR): + try: + try: + f = lookup_opts.get_field(lookup_opts.admin.list_display[int(params[ORDER_VAR])]) + except meta.FieldDoesNotExist: + pass else: - func = getattr(mod.Klass, field_name) # Let AttributeErrors propogate. - try: - header = func.short_description - except AttributeError: - header = func.__name__.replace('_', ' ') - # 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 ''), - 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.attname - 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 = '' + 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 + try: + f = lookup_opts.get_field(order_field) + except meta.FieldDoesNotExist: + pass + else: + 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. + if lookup_opts.admin.list_select_related: + lookup_params['select_related'] = True + else: + 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, the value is a method - # name. Execute the method. - func = getattr(result, field_name) - try: - result_repr = str(func()) - except ObjectDoesNotExist: - result_repr = EMPTY_CHANGELIST_VALUE - else: - # Strip HTML tags in the resulting text, except if the - # function has an "allow_tags" attribute set to True. - if not getattr(func, 'allow_tags', False): - result_repr = strip_tags(result_repr) + pass else: - field_val = getattr(result, f.attname) - # 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 and times are special: They're formatted in a certain way. - elif isinstance(f, meta.DateField) or isinstance(f, meta.TimeField): - if field_val: - (date_format, datetime_format, time_format) = get_date_formats() - if isinstance(f, meta.DateTimeField): - result_repr = capfirst(dateformat.format(field_val, datetime_format)) - elif isinstance(f, meta.TimeField): - result_repr = capfirst(dateformat.time_format(field_val, time_format)) - else: - result_repr = capfirst(dateformat.format(field_val, date_format)) - 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) + 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 - # 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('

') + if opts.one_to_one_field: + lookup_params.update(opts.one_to_one_field.rel.limit_choices_to) + self.lookup_params = lookup_params + + +def change_list(request, app_label, module_name): + try: + cl = ChangeList(request, app_label, module_name) + except IncorrectLookupParameters: + return HttpResponseRedirect(request.path) - raw_template.append('
\n
') - raw_template.append('{% endblock %}\n') - t = loader.get_template_from_string(''.join(raw_template)) c = Context(request, { - 'title': (is_popup and 'Select %s' % opts.verbose_name or 'Select %s to change' % opts.verbose_name), - 'is_popup': is_popup, + 'title': cl.title, + 'is_popup': cl.is_popup, + 'cl' : cl }) - return HttpResponse(t.render(c)) + c.update( { 'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}), + return render_to_response('admin/change_list', + context_instance = c) change_list = staff_member_required(change_list) -def _get_flattened_data(field, val): - """ - Returns a dictionary mapping the field's manipulator field names to its - "flattened" string values for the admin view. "val" is an instance of the - field's value. - """ - if isinstance(field, meta.DateTimeField): - date_field, time_field = field.get_manipulator_field_names('') - return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), - time_field: (val is not None and val.strftime("%H:%M:%S") or '')} - elif isinstance(field, meta.DateField): - return {field.name: (val is not None and val.strftime("%Y-%m-%d") or '')} - elif isinstance(field, meta.TimeField): - return {field.name: (val is not None and val.strftime("%H:%M:%S") or '')} - else: - return {field.name: val} use_raw_id_admin = lambda field: isinstance(field.rel, (meta.ManyToOne, 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_template(opts, app_label, add=False, change=False, show_delete=False, form_url=''): - admin_field_objs = opts.admin.get_field_objs(opts) - ordered_objects = opts.get_ordered_objects()[:] - auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] - t = ['{% extends "admin/base_site" %}\n'] - t.append('{% block extrahead %}') - - # Put in any necessary JavaScript imports. - javascript_imports = ['%sjs/core.js' % ADMIN_MEDIA_PREFIX, '%sjs/admin/RelatedObjectLookups.js' % ADMIN_MEDIA_PREFIX] +def get_javascript_imports(opts,auto_populated_fields, ordered_objects, field_sets): +# Put in any necessary JavaScript imports. + js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] if auto_populated_fields: - javascript_imports.append('%sjs/urlify.js' % ADMIN_MEDIA_PREFIX) + js.append('js/urlify.js') if opts.has_field_type(meta.DateTimeField) or opts.has_field_type(meta.TimeField) or opts.has_field_type(meta.DateField): - javascript_imports.extend(['%sjs/calendar.js' % ADMIN_MEDIA_PREFIX, '%sjs/admin/DateTimeShortcuts.js' % ADMIN_MEDIA_PREFIX]) + js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js']) if ordered_objects: - javascript_imports.extend(['%sjs/getElementsBySelector.js' % ADMIN_MEDIA_PREFIX, '%sjs/dom-drag.js' % ADMIN_MEDIA_PREFIX, '%sjs/admin/ordering.js' % ADMIN_MEDIA_PREFIX]) + js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) if opts.admin.js: - javascript_imports.extend(opts.admin.js) + js.extend(opts.admin.js) seen_collapse = False - for _, options in admin_field_objs: - if not seen_collapse and 'collapse' in options.get('classes', ''): + for field_set in field_sets: + if not seen_collapse and 'collapse' in field_set.classes: seen_collapse = True - javascript_imports.append('%sjs/admin/CollapsedFieldsets.js' % ADMIN_MEDIA_PREFIX) - for field_list in options['fields']: + js.append('js/admin/CollapsedFieldsets.js' ) + + for field_line in field_set: try: - for f in field_list: + for f in field_line: if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface: - javascript_imports.extend(['%sjs/SelectBox.js' % ADMIN_MEDIA_PREFIX, '%sjs/SelectFilter2.js' % ADMIN_MEDIA_PREFIX]) + js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) raise StopIteration except StopIteration: break - for j in javascript_imports: - t.append('' % j) + return js - t.append('{% endblock %}\n') - if ordered_objects: - coltype = 'colMS' - else: - coltype = 'colM' - t.append('{%% block coltype %%}%s{%% endblock %%}\n' % coltype) - t.append('{%% block bodyclass %%}%s-%s change-form{%% endblock %%}\n' % (app_label, opts.object_name.lower())) - breadcrumb_title = add and "Add %s" % opts.verbose_name or '{{ original|striptags|truncatewords:"18" }}' - t.append('{%% block breadcrumbs %%}{%% if not is_popup %%}{%% endif %%}{%% endblock %%}\n' % \ - (capfirst(opts.verbose_name_plural), breadcrumb_title)) - t.append('{% block content %}
\n') - if change: - t.append('{% if not is_popup %}') - t.append('
  • History
  • ') - if hasattr(opts.get_model_module().Klass, 'get_absolute_url'): - t.append('
  • View on site
  • ' % opts.get_content_type_id()) - t.append('
\n') - t.append('{% endif %}') - t.append('
\n' % form_url) - t.append('{% if is_popup %}{% endif %}') - if opts.admin.save_on_top: - t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects)) - t.append('{% if form.error_dict %}

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

{% endif %}\n') - for fieldset_name, options in admin_field_objs: - t.append('
\n\n' % options.get('classes', '')) - if fieldset_name: - t.append('

%s

\n' % fieldset_name) - for field_list in options['fields']: - t.append(_get_admin_field(field_list, 'form.', False, add, change)) - for f in field_list: - if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface: - t.append('\n' % (f.name, f.verbose_name, f.rel.filter_interface-1, ADMIN_MEDIA_PREFIX)) - t.append('
\n') - if ordered_objects and change: - t.append('

Ordering

') - t.append('
\n') - t.append('{% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %}') - t.append('

{{ form.order_ }}

\n') - t.append('
\n') - for rel_obj, rel_field in opts.get_inline_related_objects(): - var_name = rel_obj.object_name.lower() - field_list = [f for f in rel_obj.fields + rel_obj.many_to_many if f.editable and f != rel_field] - t.append('
\n' % ((rel_field.rel.edit_inline != meta.TABULAR) and ' aligned' or '')) - view_on_site = '' - if change and hasattr(rel_obj, 'get_absolute_url'): - view_on_site = '{%% if %s.original %%}View on site{%% endif %%}' % (var_name, var_name, var_name) - if rel_field.rel.edit_inline == meta.TABULAR: - t.append('

%s

\n\n' % capfirst(rel_obj.verbose_name_plural)) - t.append('') - for f in field_list: - if isinstance(f, meta.AutoField): - continue - t.append('%s' % (f.blank and ' class="optional"' or '', capfirst(f.verbose_name))) - t.append('\n') - t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name)) - if change: - for f in field_list: - if use_raw_id_admin(f): - t.append('{%% if %s.original %%}' % var_name) - t.append('') - t.append('' % (30, var_name)) - t.append('{% endif %}\n') - break - t.append('{%% if %s %%}\n' % ' or '.join(['%s.%s.errors' % (var_name, f.name) for f in field_list])) - t.append('\n{%% endif %%}\n' % \ - (len(field_list), ''.join(['{{ %s.%s.html_error_list }}' % (var_name, f.name) for f in field_list]))) - t.append('\n') - hidden_fields = [] - for f in field_list: - form_widget = _get_admin_field_form_widget(f, var_name+'.', True, add, change) - # Don't put AutoFields within a \n' % (var_name, f.name, form_widget)) - else: - t.append('%s\n' % (var_name, f.name, form_widget)) - else: - hidden_fields.append(form_widget) - if hasattr(rel_obj, 'get_absolute_url'): - t.append('\n' % view_on_site) - t.append('\n') - t.append('{% endfor %}\n
{{ %s.original }}
%s
, because they're hidden. - if not isinstance(f, meta.AutoField): - # Fields with raw_id_admin=True get class="nowrap". - if use_raw_id_admin(f): - t.append('%s%s
\n') - # Write out the hidden fields. We didn't write them out earlier - # because it would've been invalid HTML. - t.append('{%% for %s in form.%s %%}\n' % (var_name, rel_obj.module_name)) - t.extend(hidden_fields) - t.append('{% endfor %}\n') - else: # edit_inline == STACKED - t.append('{%% for %s in form.%s %%}' % (var_name, rel_obj.module_name)) - t.append('

%s #{{ forloop.counter }}

' % capfirst(rel_obj.verbose_name)) - if view_on_site: - t.append('

%s

' % view_on_site) - for f in field_list: - # Don't put AutoFields within the widget -- just use the field. - if isinstance(f, meta.AutoField): - t.append(_get_admin_field_form_widget(f, var_name+'.', True, add, change)) - else: - t.append(_get_admin_field([f], var_name+'.', True, add, change)) - t.append('{% endfor %}\n') - t.append('
\n') - t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects)) - if add: - # Add focus to the first field on the form, if this is an "add" form. - t.append('' % \ - admin_field_objs[0][1]['fields'][0][0].get_manipulator_field_names('')[0]) - if auto_populated_fields: - t.append('\n') - if change and ordered_objects: - t.append('{% if form.order_objects %}
    {% for object in form.order_objects %}') - t.append('
  • {{ object|truncatewords:"5" }}
  • ' % \ - {'x': ' '.join(['object.%s' % o.pk.name for o in ordered_objects])}) - t.append('{% endfor %}
{% endif %}\n') - t.append('
\n
\n{% endblock %}') - return ''.join(t) -def _get_admin_field(field_list, name_prefix, rel, add, change): - "Returns the template code for editing the given list of fields in the admin template." - field_names = [] - for f in field_list: - field_names.extend(f.get_manipulator_field_names(name_prefix)) - div_class_names = ['form-row', '{%% if %s %%} error{%% endif %%}' % ' or '.join(['%s.errors' % n for n in field_names])] - # Assumes BooleanFields won't be stacked next to each other! - if isinstance(field_list[0], meta.BooleanField): - div_class_names.append('checkbox-row') - t = [] - t.append('
\n' % ' '.join(div_class_names)) - for n in field_names: - t.append('{%% if %s.errors %%}{{ %s.html_error_list }}{%% endif %%}\n' % (n, n)) - for i, field in enumerate(field_list): - label_name = 'id_%s%s' % ((rel and "%s{{ forloop.counter0 }}." % name_prefix or ""), field.get_manipulator_field_names('')[0]) - # BooleanFields are a special case, because the checkbox widget appears to - # the *left* of the label. - if isinstance(field, meta.BooleanField): - t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change)) - t.append(' ' % (label_name, capfirst(field.verbose_name))) - else: - class_names = [] - if not field.blank: - class_names.append('required') - if i > 0: - class_names.append('inline') - t.append(' ' % (label_name, class_names and ' class="%s"' % ' '.join(class_names) or '', capfirst(field.verbose_name))) - t.append(_get_admin_field_form_widget(field, name_prefix, rel, add, change)) - if change and field.primary_key: - t.append('{{ %soriginal.%s }}' % ((rel and name_prefix or ''), field.name)) - if change and use_raw_id_admin(field): - if isinstance(field.rel, meta.ManyToOne): - if_bit = '%soriginal.get_%s' % (rel and name_prefix or '', field.name) - obj_repr = if_bit + '|truncatewords:"14"' - elif isinstance(field.rel, meta.ManyToMany): - if_bit = '%soriginal.get_%s_list' % (rel and name_prefix or '', field.name) - obj_repr = if_bit + '|join:", "|truncatewords:"14"' - t.append('{%% if %s %%} {{ %s }}{%% endif %%}' % (if_bit, obj_repr)) - if field.help_text: - t.append('

%s

\n' % field.help_text) - t.append('
\n\n') - return ''.join(t) +class AdminBoundField(BoundField): + def __init__(self, field, field_mapping, original): + super(AdminBoundField, self).__init__(field,field_mapping,original) -def _get_admin_field_form_widget(field, name_prefix, rel, add, change): - "Returns JUST the formfield widget for the field's admin interface." - field_names = field.get_manipulator_field_names(name_prefix) - if isinstance(field, meta.DateTimeField): - return '

Date: {{ %s }}
Time: {{ %s }}

' % tuple(field_names) - t = ['{{ %s }}' % n for n in field_names] - if change and isinstance(field, meta.FileField): - return '{%% if %soriginal.%s %%}Currently: {{ %soriginal.%s }}
Change: %s{%% else %%}%s{%% endif %%}' % \ - (name_prefix, field.name, name_prefix, field.name, name_prefix, field.name, ''.join(t), ''.join(t)) - field_id = 'id_%s%s' % ((rel and "%s{{ forloop.counter0 }}." % name_prefix or ""), field.get_manipulator_field_names('')[0]) - # raw_id_admin fields get the little lookup link next to them - if use_raw_id_admin(field): - t.append(' ' % \ - (field.rel.to.app_label, field.rel.to.module_name, field_id)) - t.append('Lookup' % ADMIN_MEDIA_PREFIX) - # fields with relationships to editable objects get an "add another" link, - # but only if the field doesn't have raw_admin ('cause in that case they get - # the "add" button in the popup) - elif field.rel and (isinstance(field.rel, meta.ManyToOne) or isinstance(field.rel, meta.ManyToMany)) and field.rel.to.admin: - t.append('{%% if perms.%s.%s %%}' % (field.rel.to.app_label, field.rel.to.get_add_permission())) - t.append(' ' % \ - (field.rel.to.app_label, field.rel.to.module_name, field_id)) - t.append('Add Another' % ADMIN_MEDIA_PREFIX) - t.append('{% endif %}') - return ''.join(t) + self.element_id = self.form_fields[0].get_id() + self.has_label_first = not isinstance(self.field, meta.BooleanField) + self.raw_id_admin = use_raw_id_admin(field) + self.is_date_time = isinstance(field, meta.DateTimeField) + self.is_file_field = isinstance(field, meta.FileField) + self.needs_add_label = field.rel and isinstance(field.rel, meta.ManyToOne) or isinstance(field.rel, meta.ManyToMany) and field.rel.to.admin + self.hidden = isinstance(self.field, meta.AutoField) + self.first = False + + classes = [] + if(self.raw_id_admin): + classes.append('nowrap') + if max([bool(f.errors()) for f in self.form_fields]): + classes.append('error') + if classes: + self.cell_class_attribute = ' class="%s" ' % ' '.join(classes) + self._repr_filled = False + + def _fetch_existing_display(self, func_name): + class_dict = self.original.__class__.__dict__ + func = class_dict.get(func_name) + return func(self.original) + + def _fill_existing_display(self): + if self._display_filled: + return + #HACK + if isinstance(self.field.rel, meta.ManyToOne): + func_name = 'get_%s' % self.field.name + self._display = self._fetch_existing_display(func_name) + elif isinstance(self.field.rel, meta.ManyToMany): + func_name = 'get_%s_list' % self.field.name + self._display = ",".join(self._fetch_existing_display(func_name)) + self._display_filled = True + + def existing_display(self): + self._fill_existing_display() + return self._display + + def __repr__(self): + return repr(self.__dict__) + + def html_error_list(self): + return " ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors]) + +class AdminBoundFieldLine(BoundFieldLine): + def __init__(self, field_line, field_mapping, original): + super(AdminBoundFieldLine, self).__init__(field_line, field_mapping, original, AdminBoundField) + for bound_field in self: + bound_field.first = True + break + +class AdminBoundFieldSet(BoundFieldSet): + def __init__(self, field_set, field_mapping, original): + super(AdminBoundFieldSet, self).__init__(field_set, field_mapping, original, AdminBoundFieldLine) + +class BoundManipulator(object): + def __init__(self, opts, manipulator, field_mapping): + self.inline_related_objects = opts.get_followed_related_objects(manipulator.follow) + self.original = hasattr(manipulator, 'original_object') and manipulator.original_object or None + self.bound_field_sets = [field_set.bind(field_mapping, self.original, AdminBoundFieldSet) + for field_set in opts.admin.get_field_sets(opts)] + self.ordered_objects = opts.get_ordered_objects()[:] + +class AdminBoundManipulator(BoundManipulator): + def __init__(self, opts, manipulator, field_mapping): + super(AdminBoundManipulator, self).__init__(opts, manipulator, field_mapping) + field_sets = opts.admin.get_field_sets(opts) + + self.auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] + self.javascript_imports = get_javascript_imports(opts, self.auto_populated_fields, self.ordered_objects, field_sets); + + self.coltype = self.ordered_objects and 'colMS' or 'colM' + self.has_absolute_url = hasattr(opts.get_model_module().Klass, 'get_absolute_url') + self.form_enc_attrib = opts.has_field_type(meta.FileField) and \ + 'enctype="multipart/form-data" ' or '' + + self.first_form_field_id = self.bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id(); + self.ordered_object_pk_names = [o.pk.name for o in self.ordered_objects] + + 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 get_ordered_object_pk(self, ordered_obj): + for name in self.ordered_object_pk_names: + if hasattr(ordered_obj, name): + return str(getattr(ordered_obj, name)) + return "" + +def render_change_form(opts, manipulator, app_label, context, add=False, change=False, show_delete=False, form_url=''): + extra_context = { + 'add': add, + 'change': change, + 'bound_manipulator' : AdminBoundManipulator(opts, manipulator, context['form']), + 'has_delete_permission' : context['perms'][app_label][opts.get_delete_permission()], + 'form_url' : form_url, + 'app_label': app_label, + } + context.update(extra_context) + return render_to_response(["admin/%s/%s/change_form" % (app_label, opts.object_name.lower() ), + "admin/%s/change_form" % app_label , + "admin/change_form"], context_instance=context) + +def log_add_message(user, opts,manipulator,new_object): + pk_value = getattr(new_object, opts.pk.attname) + log.log_action(user.id, opts.get_content_type_id(), pk_value, str(new_object), log.ADDITION) def add_stage(request, app_label, module_name, show_delete=False, form_url='', post_url='../', post_url_continue='../%s/', object_id_override=None): mod, opts = _get_mod_opts(app_label, module_name) @@ -780,19 +404,17 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p if opts.has_field_type(meta.FileField): new_data.update(request.FILES) errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) + if not errors and not request.POST.has_key("_preview"): - for f in opts.many_to_many: - if f.rel.raw_id_admin: - new_data.setlist(f.name, new_data[f.name].split(",")) - manipulator.do_html2python(new_data) new_object = manipulator.save(new_data) - pk_value = getattr(new_object, opts.pk.attname) - log.log_action(request.user.id, opts.get_content_type_id(), pk_value, repr(new_object), log.ADDITION) - msg = 'The %s "%s" was added successfully.' % (opts.verbose_name, new_object) + log_add_message(request.user, opts,manipulator,new_object) + msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name':opts.verbose_name, 'obj':new_object} + pk_value = getattr(new_object,opts.pk.attname) # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. if request.POST.has_key("_continue"): - request.user.add_message("%s You may edit it again below." % msg) + request.user.add_message(msg + ' ' + _("You may edit it again below.")) if request.POST.has_key("_popup"): post_url_continue += "?_popup=1" return HttpResponseRedirect(post_url_continue % pk_value) @@ -800,70 +422,49 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p return HttpResponse('' % \ (pk_value, repr(new_object).replace('"', '\\"'))) elif request.POST.has_key("_addanother"): - request.user.add_message("%s You may add another %s below." % (msg, opts.verbose_name)) + request.user.add_message(msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) return HttpResponseRedirect(request.path) else: request.user.add_message(msg) return HttpResponseRedirect(post_url) - if request.POST.has_key("_preview"): - manipulator.do_html2python(new_data) else: - new_data = {} # Add default data. - for f in opts.fields: - if f.has_default(): - new_data.update(_get_flattened_data(f, f.get_default())) - # In required many-to-one fields with only one available choice, - # select that one available choice. Note: We have to check that - # the length of choices is *2*, not 1, because SelectFields always - # have an initial "blank" value. - elif not f.blank and ((isinstance(f.rel, meta.ManyToOne) and not f.rel.raw_id_admin) or f.choices) and len(manipulator[f.name].choices) == 2: - new_data[f.name] = manipulator[f.name].choices[1][0] - # In required many-to-many fields with only one available choice, - # select that one available choice. - for f in opts.many_to_many: - if not f.blank and not f.rel.edit_inline and not f.rel.raw_id_admin and len(manipulator[f.name].choices) == 1: - new_data[f.name] = [manipulator[f.name].choices[0][0]] - # Add default data for related objects. - for rel_opts, rel_field in opts.get_inline_related_objects(): - var_name = rel_opts.object_name.lower() - for i in range(rel_field.rel.num_in_admin): - for f in rel_opts.fields + rel_opts.many_to_many: - if f.has_default(): - for field_name in f.get_manipulator_field_names(''): - new_data['%s.%d.%s' % (var_name, i, field_name)] = f.get_default() + new_data = manipulator.flatten_data() + # Override the defaults with request.GET, if it exists. new_data.update(request.GET) errors = {} # Populate the FormWrapper. - form = formfields.FormWrapper(manipulator, new_data, errors) - for rel_opts, rel_field in opts.get_inline_related_objects(): - var_name = rel_opts.object_name.lower() - wrapper = [] - for i in range(rel_field.rel.num_in_admin): - collection = {} - for f in rel_opts.fields + rel_opts.many_to_many: - if f.editable and f != rel_field and not isinstance(f, meta.AutoField): - for field_name in f.get_manipulator_field_names(''): - full_field_name = '%s.%d.%s' % (var_name, i, field_name) - collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, ''), errors.get(full_field_name, [])) - wrapper.append(formfields.FormFieldCollection(collection)) - setattr(form, rel_opts.module_name, wrapper) + form = formfields.FormWrapper(manipulator, new_data, errors, edit_inline=True) c = Context(request, { - 'title': 'Add %s' % opts.verbose_name, - "form": form, - "is_popup": request.REQUEST.has_key("_popup"), + 'title': _('Add %s') % opts.verbose_name, + 'form': form, + 'is_popup': request.REQUEST.has_key('_popup'), + 'show_delete': show_delete, }) if object_id_override is not None: c['object_id'] = object_id_override - raw_template = _get_template(opts, app_label, add=True, show_delete=show_delete, form_url=form_url) -# return HttpResponse(raw_template, mimetype='text/plain') - t = loader.get_template_from_string(raw_template) - return HttpResponse(t.render(c)) + + 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): + pk_value = getattr(new_object, opts.pk.column) + # Construct the change message. + change_message = [] + if manipulator.fields_added: + change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and'))) + if manipulator.fields_changed: + change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and'))) + if manipulator.fields_deleted: + change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and'))) + change_message = ' '.join(change_message) + if not change_message: + change_message = _('No fields changed.') + log.log_action(user.id, opts.get_content_type_id(), pk_value, str(new_object), log.CHANGE, change_message) + def change_stage(request, app_label, module_name, object_id): mod, opts = _get_mod_opts(app_label, module_name) if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): @@ -875,120 +476,72 @@ def change_stage(request, app_label, module_name, object_id): except ObjectDoesNotExist: raise Http404 - inline_related_objects = opts.get_inline_related_objects() if request.POST: new_data = request.POST.copy() if opts.has_field_type(meta.FileField): new_data.update(request.FILES) errors = manipulator.get_validation_errors(new_data) + + manipulator.do_html2python(new_data) if not errors and not request.POST.has_key("_preview"): - for f in opts.many_to_many: - if f.rel.raw_id_admin: - new_data.setlist(f.name, new_data[f.name].split(",")) - manipulator.do_html2python(new_data) new_object = manipulator.save(new_data) - pk_value = getattr(new_object, opts.pk.attname) - - # Construct the change message. - change_message = [] - if manipulator.fields_added: - change_message.append('Added %s.' % get_text_list(manipulator.fields_added, 'and')) - if manipulator.fields_changed: - change_message.append('Changed %s.' % get_text_list(manipulator.fields_changed, 'and')) - if manipulator.fields_deleted: - change_message.append('Deleted %s.' % get_text_list(manipulator.fields_deleted, 'and')) - change_message = ' '.join(change_message) - if not change_message: - change_message = 'No fields changed.' - - log.log_action(request.user.id, opts.get_content_type_id(), pk_value, repr(new_object), log.CHANGE, change_message) - msg = 'The %s "%s" was changed successfully.' % (opts.verbose_name, new_object) + log_change_message(request.user,opts,manipulator,new_object) + msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj':new_object} + pk_value = getattr(new_object,opts.pk.attname) if request.POST.has_key("_continue"): - request.user.add_message("%s You may edit it again below." % msg) + request.user.add_message(msg + ' ' + _("You may edit it again below.")) if request.REQUEST.has_key('_popup'): return HttpResponseRedirect(request.path + "?_popup=1") else: return HttpResponseRedirect(request.path) elif request.POST.has_key("_saveasnew"): - request.user.add_message('The %s "%s" was added successfully. You may edit it again below.' % (opts.verbose_name, new_object)) + request.user.add_message(_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object}) return HttpResponseRedirect("../%s/" % pk_value) elif request.POST.has_key("_addanother"): - request.user.add_message("%s You may add another %s below." % (msg, opts.verbose_name)) + request.user.add_message(msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) return HttpResponseRedirect("../add/") else: request.user.add_message(msg) return HttpResponseRedirect("../") - if request.POST.has_key("_preview"): - manipulator.do_html2python(new_data) else: # Populate new_data with a "flattened" version of the current data. - new_data = {} - obj = manipulator.original_object - for f in opts.fields: - new_data.update(_get_flattened_data(f, getattr(obj, f.attname))) - for f in opts.many_to_many: - get_list_func = getattr(obj, 'get_%s_list' % f.rel.singular) - if f.rel.raw_id_admin: - new_data[f.name] = ",".join([str(getattr(i, f.rel.to.pk.attname)) for i in get_list_func()]) - elif not f.rel.edit_inline: - new_data[f.name] = [getattr(i, f.rel.to.pk.attname) for i in get_list_func()] - for rel_obj, rel_field in inline_related_objects: - var_name = rel_obj.object_name.lower() - for i, rel_instance in enumerate(getattr(obj, 'get_%s_list' % opts.get_rel_object_method_name(rel_obj, rel_field))()): - for f in rel_obj.fields: - if f.editable and f != rel_field: - for k, v in _get_flattened_data(f, getattr(rel_instance, f.attname)).items(): - new_data['%s.%d.%s' % (var_name, i, k)] = v - for f in rel_obj.many_to_many: - new_data['%s.%d.%s' % (var_name, i, f.column)] = [j.id for j in getattr(rel_instance, 'get_%s_list' % f.rel.singular)()] + new_data = manipulator.flatten_data() + # TODO: do this in flatten_data... # If the object has ordered objects on its admin page, get the existing # order and flatten it into a comma-separated list of IDs. + id_order_list = [] for rel_obj in opts.get_ordered_objects(): - id_order_list.extend(getattr(obj, 'get_%s_order' % rel_obj.object_name.lower())()) + 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 = {} # Populate the FormWrapper. - form = formfields.FormWrapper(manipulator, new_data, errors) + form = formfields.FormWrapper(manipulator, new_data, errors, edit_inline = True) form.original = manipulator.original_object form.order_objects = [] - for rel_opts, rel_field in inline_related_objects: - var_name = rel_opts.object_name.lower() - wrapper = [] - orig_list = getattr(manipulator.original_object, 'get_%s_list' % opts.get_rel_object_method_name(rel_opts, rel_field))() - count = len(orig_list) + rel_field.rel.num_extra_on_change - if rel_field.rel.min_num_in_admin: - count = max(count, rel_field.rel.min_num_in_admin) - if rel_field.rel.max_num_in_admin: - count = min(count, rel_field.rel.max_num_in_admin) - for i in range(count): - collection = {'original': (i < len(orig_list) and orig_list[i] or None)} - for f in rel_opts.fields + rel_opts.many_to_many: - if f.editable and f != rel_field: - for field_name in f.get_manipulator_field_names(''): - full_field_name = '%s.%d.%s' % (var_name, i, field_name) - collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, f.get_default()), errors.get(full_field_name, [])) - wrapper.append(formfields.FormFieldCollection(collection)) - setattr(form, rel_opts.module_name, wrapper) - if rel_opts.order_with_respect_to and rel_opts.order_with_respect_to.rel and rel_opts.order_with_respect_to.rel.to == opts: + + #TODO Should be done in flatten_data / FormWrapper construction + for related in opts.get_followed_related_objects(): + wrt = related.opts.order_with_respect_to + if wrt and wrt.rel and wrt.rel.to == opts: + func = getattr(manipulator.original_object, 'get_%s_list' % + related.get_method_name_part()) + orig_list = func() form.order_objects.extend(orig_list) c = Context(request, { - 'title': 'Change %s' % opts.verbose_name, - "form": form, + 'title': _('Change %s') % opts.verbose_name, + 'form': form, 'object_id': object_id, 'original': manipulator.original_object, - 'is_popup' : request.REQUEST.has_key('_popup'), + 'is_popup' : request.REQUEST.has_key('_popup') }) - raw_template = _get_template(opts, app_label, change=True) -# return HttpResponse(raw_template, mimetype='text/plain') - t = loader.get_template_from_string(raw_template) - return HttpResponse(t.render(c)) -change_stage = staff_member_required(change_stage) + + return render_change_form(opts,manipulator, app_label, c, change=True) def _nest_help(obj, depth, val): current = obj @@ -1002,75 +555,77 @@ def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current if current_depth > 16: return # Avoid recursing too deep. objects_seen = [] - for rel_opts, rel_field in opts.get_all_related_objects(): - if rel_opts in objects_seen: + 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(rel_opts, rel_field) - if isinstance(rel_field.rel, meta.OneToOne): + 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)() except ObjectDoesNotExist: pass else: if rel_opts.admin: - p = '%s.%s' % (rel_opts.app_label, rel_opts.get_delete_permission()) + 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) + perms_needed.add(related.opts.verbose_name) # We don't care about populating deleted_objects now. continue - 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, ['%s: %r' % (capfirst(rel_opts.verbose_name), sub_obj), []]) + nh(deleted_objects, current_depth, ['%s: %s' % (capfirst(related.opts.verbose_name), sub_obj), []]) else: # Display a link to the admin page. - nh(deleted_objects, current_depth, ['%s: %r' % \ - (capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, - getattr(sub_obj, rel_opts.pk.attname), sub_obj), []]) - _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2) + nh(deleted_objects, current_depth, ['%s: %s' % \ + (capfirst(related.opts.verbose_name), related.opts.app_label, related.opts.module_name, + getattr(sub_obj, related.opts.pk.attname), sub_obj), []]) + _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2) else: has_related_objs = False for sub_obj in getattr(obj, 'get_%s_list' % rel_opts_name)(): 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, ['%s: %s' % (capfirst(rel_opts.verbose_name), strip_tags(repr(sub_obj))), []]) + nh(deleted_objects, current_depth, ['%s: %s' % (capfirst(related.opts.verbose_name), strip_tags(str(sub_obj))), []]) else: # Display a link to the admin page. nh(deleted_objects, current_depth, ['%s: %s' % \ - (capfirst(rel_opts.verbose_name), rel_opts.app_label, rel_opts.module_name, sub_obj.id, strip_tags(repr(sub_obj))), []]) - _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, rel_opts, current_depth+2) + (capfirst(related.opts.verbose_name), related.opts.app_label, related.opts.module_name, sub_obj.id, strip_tags(str(sub_obj))), []]) + _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2) # If there were related objects, and the user doesn't have # permission to delete them, add the missing perm to perms_needed. - if rel_opts.admin and has_related_objs: - p = '%s.%s' % (rel_opts.app_label, rel_opts.get_delete_permission()) + if related.opts.admin and has_related_objs: + 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))), []]) + nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \ + {'fieldname': related.field.name, 'name': related.opts.verbose_name, 'obj': strip_tags(str(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))), []]) + nh(deleted_objects, current_depth, [ + (_('One or more %(fieldname)s in %(name)s:') % {'fieldname': related.field.name, 'name':related.opts.verbose_name}) + \ + (' %s' % \ + (related.opts.app_label, related.opts.module_name, sub_obj.id, strip_tags(str(sub_obj)))), []]) # If there were related objects, and the user doesn't have # permission to change them, add the missing perm to perms_needed. - if 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 @@ -1081,20 +636,20 @@ def delete_stage(request, app_label, module_name, object_id): # Populate deleted_objects, a data structure of all related objects that # will also be deleted. - deleted_objects = ['%s: %s' % (capfirst(opts.verbose_name), object_id, strip_tags(repr(obj))), []] + deleted_objects = ['%s: %s' % (capfirst(opts.verbose_name), object_id, strip_tags(str(obj))), []] perms_needed = sets.Set() _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1) if request.POST: # The user has already confirmed the deletion. if perms_needed: raise PermissionDenied - obj_repr = repr(obj) + obj_display = str(obj) obj.delete() - log.log_action(request.user.id, opts.get_content_type_id(), object_id, obj_repr, log.DELETION) - request.user.add_message('The %s "%s" was deleted successfully.' % (opts.verbose_name, obj_repr)) + log.log_action(request.user.id, opts.get_content_type_id(), object_id, obj_display, log.DELETION) + request.user.add_message(_('The %(name)s "%(obj)s" was deleted successfully.') % {'name':opts.verbose_name, 'obj':obj_display}) return HttpResponseRedirect("../../") return render_to_response('admin/delete_confirmation', { - "title": "Are you sure?", + "title": _("Are you sure?"), "object_name": opts.verbose_name, "object": obj, "deleted_objects": deleted_objects, @@ -1109,7 +664,7 @@ def history(request, app_label, module_name, object_id): # If no history was found, see whether this object even exists. obj = get_object_or_404(mod, pk=object_id) return render_to_response('admin/object_history', { - 'title': 'Change history: %r' % obj, + 'title': _('Change history: %s') % obj, 'action_list': action_list, 'module_name': capfirst(opts.verbose_name_plural), 'object': obj, diff --git a/django/core/formfields.py b/django/core/formfields.py index 78fd044815..0cfe6b2890 100644 --- a/django/core/formfields.py +++ b/django/core/formfields.py @@ -90,15 +90,7 @@ class Manipulator: 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, []) + field.convert_post_data(new_data) class FormWrapper: """ @@ -106,24 +98,36 @@ class FormWrapper: 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): + def __init__(self, manipulator, data, error_dict, edit_inline=True): self.manipulator, self.data = manipulator, data self.error_dict = error_dict + self._inline_collections = None + self.edit_inline = edit_inline def __repr__(self): - return repr(self.data) + return repr(self.__dict__) 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 = '' + data = field.extract_data(self.data) return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, [])) - raise KeyError + if self.edit_inline: + self.fill_inline_collections() + for inline_collection in self._inline_collections: + if inline_collection.name == key: + return inline_collection + raise KeyError, "Could not find Formfield or InlineObjectCollection named %r" % key + + def fill_inline_collections(self): + if not self._inline_collections: + ic = [] + related_objects = self.manipulator.get_related_objects() + for rel_obj in related_objects: + data = rel_obj.extract_data(self.data) + inline_collection = InlineObjectCollection(self.manipulator, rel_obj, data, self.error_dict) + ic.append(inline_collection) + self._inline_collections = ic def has_errors(self): return self.error_dict != {} @@ -166,6 +170,9 @@ class FormFieldWrapper: else: return '' + def get_id(self): + return self.formfield.get_id() + class FormFieldCollection(FormFieldWrapper): "A utility class that gives the template access to a dict of FormFieldWrappers" def __init__(self, formfield_dict): @@ -185,9 +192,66 @@ class FormFieldCollection(FormFieldWrapper): "Returns list of all errors in this collection's formfields" errors = [] for field in self.formfield_dict.values(): - errors.extend(field.errors()) + if hasattr(field, 'errors'): + errors.extend(field.errors()) return errors + def has_errors(self): + return bool(len(self.errors())) + + def html_combined_error_list(self): + return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]) + +class InlineObjectCollection: + "An object that acts like a list of form field collections." + def __init__(self, parent_manipulator, rel_obj, data, errors): + self.parent_manipulator = parent_manipulator + self.rel_obj = rel_obj + self.data = data + self.errors = errors + self._collections = None + self.name = rel_obj.name + + def __len__(self): + self.fill() + return self._collections.__len__() + + def __getitem__(self, k): + self.fill() + return self._collections.__getitem__(k) + + def __setitem__(self, k, v): + self.fill() + return self._collections.__setitem__(k,v) + + def __delitem__(self, k): + self.fill() + return self._collections.__delitem__(k) + + def __iter__(self): + self.fill() + return self._collections.__iter__() + + def fill(self): + if self._collections: + return + else: + var_name = self.rel_obj.opts.object_name.lower() + wrapper = [] + orig = hasattr(self.parent_manipulator, 'original_object') and self.parent_manipulator.original_object or None + orig_list = self.rel_obj.get_list(orig) + for i, instance in enumerate(orig_list): + collection = {'original': instance} + for f in self.rel_obj.editable_fields(): + for field_name in f.get_manipulator_field_names(''): + full_field_name = '%s.%d.%s' % (var_name, i, field_name) + field = self.parent_manipulator[full_field_name] + data = field.extract_data(self.data) + errors = self.errors.get(full_field_name, []) + collection[field_name] = FormFieldWrapper(field, data, errors) + wrapper.append(FormFieldCollection(collection)) + self._collections = wrapper + class FormField: """Abstract class representing a form field. @@ -220,6 +284,37 @@ class FormField: def render(self, data): raise NotImplementedError + def get_member_name(self): + if hasattr(self, 'member_name'): + return self.member_name + else: + return self.field_name + + def extract_data(self, data_dict): + if hasattr(self, 'requires_data_list') and hasattr(data_dict, 'getlist'): + data = data_dict.getlist(self.get_member_name()) + else: + data = data_dict.get(self.get_member_name(), None) + if data is None: + data = '' + return data + + def convert_post_data(self, new_data): + name = self.get_member_name() + if new_data.has_key(self.field_name): + d = new_data.getlist(self.field_name) + try: + converted_data = [self.__class__.html2python(data) for data in d] + except ValueError: + converted_data = d + new_data.setlist(name, converted_data) + else: + try: + # individual fields deal with None values themselves + new_data.setlist(name, [self.__class__.html2python(None)]) + except EmptyValue: + new_data.setlist(name, []) + def get_id(self): "Returns the HTML 'id' attribute for this form field." return FORM_FIELD_ID_PREFIX + self.field_name @@ -313,11 +408,13 @@ class CheckboxField(FormField): html2python = staticmethod(html2python) class SelectField(FormField): - def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=[]): + def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=[], member_name=None): 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 + if member_name != None: + self.member_name = member_name def render(self, data): output = [' ' % \ - (self.get_id(), self.__class__.__name__, field_name, checked_html, - self.get_id(), choice)) + (self.get_id() + value , self.__class__.__name__, field_name, checked_html, + self.get_id() + value, choice)) output.append('') return '\n'.join(output) @@ -528,8 +627,10 @@ class ImageUploadField(FileUploadField): #################### class IntegerField(TextField): - def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]): + def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[], member_name=None): validator_list = [self.isInteger] + validator_list + if member_name is not None: + self.member_name = member_name TextField.__init__(self, field_name, length, maxlength, is_required, validator_list) def isInteger(self, field_data, all_data): @@ -784,6 +885,11 @@ 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): """ A LargeTextField with an XML validator. The schema_path argument is the diff --git a/django/core/management.py b/django/core/management.py index efc1d9f42c..d34ee6fc4d 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -670,12 +670,12 @@ def get_validation_errors(outfile): e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name) # Check core=True, if needed. - for rel_opts, rel_field in opts.get_inline_related_objects(): + for related in opts.get_followed_related_objects(): try: - for f in rel_opts.fields: + for f in related.opts.fields: if f.core: raise StopIteration - e.add(rel_opts, "At least one field in %s should have core=True, because it's being edited inline by %s.%s." % (rel_opts.object_name, opts.module_name, opts.object_name)) + e.add(related.opts, "At least one field in %s should have core=True, because it's being edited inline by %s.%s." % (related.opts.object_name, opts.module_name, opts.object_name)) except StopIteration: pass diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index b6a148bcf6..76830d0c02 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -148,6 +148,140 @@ class FieldDoesNotExist(Exception): class BadKeywordArguments(Exception): pass +class BoundRelatedObject(object): + def __init__(self, related_object, field_mapping, original): + self.relation = related_object + self.field_mappings = field_mapping[related_object.opts.module_name] + + def template_name(self): + raise NotImplementedError + + def __repr__(self): + return repr(self.__dict__) + +class RelatedObject(object): + def __init__(self, parent_opts, opts, field): + self.parent_opts = parent_opts + self.opts = opts + self.field = field + self.edit_inline = field.rel.edit_inline + self.name = opts.module_name + self.var_name = opts.object_name.lower() + + def flatten_data(self, follow, obj=None): + new_data = {} + rel_instances = self.get_list(obj) + for i, rel_instance in enumerate(rel_instances): + instance_data = {} + for f in self.opts.fields + self.opts.many_to_many: + # TODO: Fix for recursive manipulators. + fol = follow.get(f.name, None) + if fol: + field_data = f.flatten_data(fol, rel_instance) + for name, value in field_data.items(): + instance_data['%s.%d.%s' % (self.var_name, i, name)] = value + new_data.update(instance_data) + return new_data + + def extract_data(self, data): + """ + Pull out the data meant for inline objects of this class, + i.e. anything starting with our module name. + """ + return data # TODO + + def get_list(self, parent_instance=None): + "Get the list of this type of object from an instance of the parent class." + if parent_instance != None: + func_name = 'get_%s_list' % self.get_method_name_part() + func = getattr(parent_instance, func_name) + list = func() + + count = len(list) + self.field.rel.num_extra_on_change + if self.field.rel.min_num_in_admin: + count = max(count, self.field.rel.min_num_in_admin) + if self.field.rel.max_num_in_admin: + count = min(count, self.field.rel.max_num_in_admin) + + change = count - len(list) + if change > 0: + return list + [None for _ in range(change)] + if change < 0: + return list[:change] + else: # Just right + return list + else: + return [None for _ in range(self.field.rel.num_in_admin)] + + + def editable_fields(self): + "Get the fields in this class that should be edited inline." + return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] + + def get_follow(self, override=None): + if isinstance(override, bool): + if override: + over = {} + else: + return None + else: + if override: + over = override.copy() + elif self.edit_inline: + over = {} + else: + return None + + over[self.field.name] = False + return self.opts.get_follow(over) + + def __repr__(self): + return "" % ( self.name, self.field.name) + + def get_manipulator_fields(self, opts, manipulator, change, follow): + # TODO: Remove core fields stuff. + if change: + meth_name = 'get_%s_count' % self.get_method_name_part() + count = getattr(manipulator.original_object, meth_name)() + count += self.field.rel.num_extra_on_change + if self.field.rel.min_num_in_admin: + count = max(count, self.field.rel.min_num_in_admin) + if self.field.rel.max_num_in_admin: + count = min(count, self.field.rel.max_num_in_admin) + else: + count = self.field.rel.num_in_admin + + fields = [] + for i in range(count): + for f in self.opts.fields + self.opts.many_to_many: + if follow.get(f.name, False): + prefix = '%s.%d.' % (self.var_name, i) + fields.extend(f.get_manipulator_fields(self.opts, manipulator, change, name_prefix=prefix, rel=True)) + return fields + + def bind(self, field_mapping, original, bound_related_object_class=BoundRelatedObject): + return bound_related_object_class(self, field_mapping, original) + + def get_method_name_part(self): + # This method encapsulates the logic that decides what name to give a + # method that retrieves related many-to-one objects. Usually it just + # uses the lower-cased object_name, but if the related object is in + # another app, its app_label is appended. + # + # Examples: + # + # # Normal case -- a related object in the same app. + # # This method returns "choice". + # Poll.get_choice_list() + # + # # A related object in a different app. + # # This method returns "lcom_bestofaward". + # Place.get_lcom_bestofaward_list() # "lcom_bestofaward" + rel_obj_name = self.field.rel.related_name or self.opts.object_name.lower() + if self.parent_opts.app_label != self.opts.app_label: + rel_obj_name = '%s_%s' % (self.opts.app_label, rel_obj_name) + return rel_obj_name + class 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, @@ -268,26 +402,6 @@ class Options: def get_delete_permission(self): return 'delete_%s' % self.object_name.lower() - def get_rel_object_method_name(self, rel_opts, rel_field): - # This method encapsulates the logic that decides what name to give a - # method that retrieves related many-to-one objects. Usually it just - # uses the lower-cased object_name, but if the related object is in - # another app, its app_label is appended. - # - # Examples: - # - # # Normal case -- a related object in the same app. - # # This method returns "choice". - # Poll.get_choice_list() - # - # # A related object in a different app. - # # This method returns "lcom_bestofaward". - # Place.get_lcom_bestofaward_list() # "lcom_bestofaward" - rel_obj_name = rel_field.rel.related_name or rel_opts.object_name.lower() - if self.app_label != rel_opts.app_label: - rel_obj_name = '%s_%s' % (rel_opts.app_label, rel_obj_name) - return rel_obj_name - def get_all_related_objects(self): try: # Try the cache first. return self._all_related_objects @@ -298,7 +412,7 @@ class Options: for klass in mod._MODELS: for f in klass._meta.fields: if f.rel and self == f.rel.to: - rel_objs.append((klass._meta, f)) + rel_objs.append(RelatedObject(self, klass._meta, f)) if self.has_related_links: # Manually add RelatedLink objects, which are a special case. relatedlinks = get_module('relatedlinks', 'relatedlinks') @@ -312,12 +426,31 @@ class Options: 'content_type__package__label__exact': self.app_label, 'content_type__python_module_name__exact': self.module_name, }) - rel_objs.append((relatedlinks.RelatedLink._meta, link_field)) + rel_objs.append(RelatedObject(self, relatedlinks.RelatedLink._meta, link_field)) self._all_related_objects = rel_objs return rel_objs - def get_inline_related_objects(self): - return [(a, b) for a, b in self.get_all_related_objects() if b.rel.edit_inline] + def get_followed_related_objects(self, follow=None): + if follow == None: + follow = self.get_follow() + return [f for f in self.get_all_related_objects() if follow.get(f.name, None)] + + def get_data_holders(self, follow=None): + if follow == None: + follow = self.get_follow() + return [f for f in self.fields + self.many_to_many + self.get_all_related_objects() if follow.get(f.name, None)] + + def get_follow(self, override=None): + follow = {} + for f in self.fields + self.many_to_many + self.get_all_related_objects(): + if override and override.has_key(f.name): + child_override = override[f.name] + else: + child_override = None + fol = f.get_follow(child_override) + if fol: + follow[f.name] = fol + return follow def get_all_related_many_to_many_objects(self): module_list = get_installed_model_modules() @@ -327,7 +460,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 @@ -345,11 +478,12 @@ class Options: self._ordered_objects = objects return self._ordered_objects - def has_field_type(self, field_type): + def has_field_type(self, field_type, follow=None): """ Returns True if this object's admin form has at least one of the given field_type (e.g. FileField). """ + # TODO: follow if not hasattr(self, '_field_types'): self._field_types = {} if not self._field_types.has_key(field_type): @@ -359,8 +493,8 @@ class Options: if isinstance(f, field_type): raise StopIteration # Failing that, check related fields. - for rel_obj, rel_field in self.get_inline_related_objects(): - for f in rel_obj.fields: + for related in self.get_followed_related_objects(follow): + for f in related.opts.fields: if isinstance(f, field_type): raise StopIteration except StopIteration: @@ -597,6 +731,7 @@ class ModelBase(type): new_mod.get_latest = curry(function_get_latest, opts, new_class, does_not_exist_exception) for f in opts.fields: + #TODO : change this into a virtual function so that user defined fields will be able to add methods to module or class. if f.choices: # Add "get_thingie_display" method to get human-readable value. func = curry(method_get_display_value, f) @@ -720,12 +855,9 @@ class ModelBase(type): old_app._MODELS[i] = new_class # Replace all relationships to the old class with # relationships to the new one. - for rel_opts, rel_field in model._meta.get_all_related_objects(): - rel_field.rel.to = opts - for rel_opts, rel_field in model._meta.get_all_related_many_to_many_objects(): - rel_field.rel.to = opts + for related in model._meta.get_all_related_objects() + model._meta.get_all_related_many_to_many_objects(): + related.field.rel.to = opts break - return new_class class Model: @@ -826,9 +958,9 @@ def method_delete(opts, self): if hasattr(self, '_pre_delete'): self._pre_delete() cursor = db.db.cursor() - for rel_opts, rel_field in opts.get_all_related_objects(): - rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field) - if isinstance(rel_field.rel, OneToOne): + for related in opts.get_all_related_objects(): + rel_opts_name = related.get_method_name_part() + if isinstance(related.field.rel, OneToOne): try: sub_obj = getattr(self, 'get_%s' % rel_opts_name)() except ObjectDoesNotExist: @@ -838,9 +970,9 @@ 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(): + for related in opts.get_all_related_many_to_many_objects(): cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ - (db.db.quote_name(rel_field.get_m2m_db_table(rel_opts)), + (db.db.quote_name(related.field.get_m2m_db_table(related.opts)), db.db.quote_name(self._meta.object_name.lower() + '_id')), [getattr(self, opts.pk.attname)]) for f in opts.many_to_many: cursor.execute("DELETE FROM %s WHERE %s=%%s" % \ @@ -1474,6 +1606,8 @@ def get_manipulator(opts, klass, extra_methods, add=False, change=False): man.__module__ = MODEL_PREFIX + '.' + opts.module_name # Set this explicitly, as above. man.__init__ = curry(manipulator_init, opts, add, change) man.save = curry(manipulator_save, opts, klass, add, change) + man.get_related_objects = curry(manipulator_get_related_objects, opts, klass, add, change) + man.flatten_data = curry(manipulator_flatten_data, opts, klass, add, change) for field_name_list in opts.unique_together: setattr(man, 'isUnique%s' % '_'.join(field_name_list), curry(manipulator_validator_unique_together, field_name_list, opts)) for f in opts.fields: @@ -1487,7 +1621,9 @@ def get_manipulator(opts, klass, extra_methods, add=False, change=False): setattr(man, k, v) return man -def manipulator_init(opts, add, change, self, obj_key=None): +def manipulator_init(opts, add, change, self, obj_key=None, follow=None): + self.follow = opts.get_follow(follow) + if change: assert obj_key is not None, "ChangeManipulator.__init__() must be passed obj_key parameter." self.obj_key = obj_key @@ -1511,40 +1647,37 @@ def manipulator_init(opts, add, change, self, obj_key=None): else: raise self.fields = [] + for f in opts.fields + opts.many_to_many: - if f.editable and not (f.primary_key and change) and (not f.rel or not f.rel.edit_inline): + if self.follow.get(f.name, False): self.fields.extend(f.get_manipulator_fields(opts, self, change)) # Add fields for related objects. - for rel_opts, rel_field in opts.get_inline_related_objects(): - if change: - count = getattr(self.original_object, 'get_%s_count' % opts.get_rel_object_method_name(rel_opts, rel_field))() - count += rel_field.rel.num_extra_on_change - if rel_field.rel.min_num_in_admin: - count = max(count, rel_field.rel.min_num_in_admin) - if rel_field.rel.max_num_in_admin: - count = min(count, rel_field.rel.max_num_in_admin) - else: - count = rel_field.rel.num_in_admin - for f in rel_opts.fields + rel_opts.many_to_many: - if f.editable and f != rel_field and (not f.primary_key or (f.primary_key and change)): - for i in range(count): - self.fields.extend(f.get_manipulator_fields(rel_opts, self, change, name_prefix='%s.%d.' % (rel_opts.object_name.lower(), i), rel=True)) + for f in opts.get_all_related_objects(): + if self.follow.get(f.name, False): + fol = self.follow[f.name] + self.fields.extend(f.get_manipulator_fields(opts, self, change, fol)) # Add field for ordering. if change and opts.get_ordered_objects(): self.fields.append(formfields.CommaSeparatedIntegerField(field_name="order_")) def manipulator_save(opts, klass, add, change, self, new_data): + # TODO: big cleanup when core fields go -> use recursive manipulators. from django.utils.datastructures import DotExpandedDict params = {} for f in opts.fields: - # Fields with auto_now_add are another special case; they should keep - # their original value in the change stage. - if change and getattr(f, 'auto_now_add', False): - params[f.attname] = getattr(self.original_object, f.attname) + # Fields with auto_now_add should keep their original value in the change stage. + auto_now_add = change and getattr(f, 'auto_now_add', False) + if self.follow.get(f.name, None) and not auto_now_add: + param = f.get_manipulator_new_data(new_data) else: - params[f.attname] = f.get_manipulator_new_data(new_data) + if change: + param = getattr(self.original_object, f.attname) + else: + param = f.get_default() + params[f.attname] = param + if change: params[opts.pk.attname] = self.obj_key @@ -1567,101 +1700,116 @@ def manipulator_save(opts, klass, add, change, self, new_data): # Save many-to-many objects. Example: Poll.set_sites() for f in opts.many_to_many: - if not f.rel.edit_inline: - was_changed = getattr(new_object, 'set_%s' % f.name)(new_data.getlist(f.name)) - if change and was_changed: - self.fields_changed.append(f.verbose_name) + if self.follow.get(f.name, None): + if not f.rel.edit_inline: + was_changed = getattr(new_object, 'set_%s' % f.name)(new_data.getlist(f.name)) + if change and was_changed: + self.fields_changed.append(f.verbose_name) + expanded_data = DotExpandedDict(new_data.data) # Save many-to-one objects. Example: Add the Choice objects for a Poll. - for rel_opts, rel_field in opts.get_inline_related_objects(): + for related in opts.get_all_related_objects(): # Create obj_list, which is a DotExpandedDict such as this: # [('0', {'id': ['940'], 'choice': ['This is the first choice']}), # ('1', {'id': ['941'], 'choice': ['This is the second choice']}), # ('2', {'id': [''], 'choice': ['']})] - obj_list = DotExpandedDict(new_data.data)[rel_opts.object_name.lower()].items() - obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0]))) - params = {} + child_follow = self.follow.get(related.name, None) - # For each related item... - for _, rel_new_data in obj_list: + if child_follow: + obj_list = expanded_data[related.var_name].items() + obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0]))) + params = {} - # Keep track of which core=True fields were provided. - # If all core fields were given, the related object will be saved. - # If none of the core fields were given, the object will be deleted. - # If some, but not all, of the fields were given, the validator would - # have caught that. - all_cores_given, all_cores_blank = True, True - # Get a reference to the old object. We'll use it to compare the - # old to the new, to see which fields have changed. - if change: + # For each related item... + for _, rel_new_data in obj_list: + + # Keep track of which core=True fields were provided. + # If all core fields were given, the related object will be saved. + # If none of the core fields were given, the object will be deleted. + # If some, but not all, of the fields were given, the validator would + # have caught that. + all_cores_given, all_cores_blank = True, True + + # Get a reference to the old object. We'll use it to compare the + # old to the new, to see which fields have changed. old_rel_obj = None - if rel_new_data[rel_opts.pk.name][0]: - try: - old_rel_obj = getattr(self.original_object, 'get_%s' % opts.get_rel_object_method_name(rel_opts, rel_field))(**{'%s__exact' % rel_opts.pk.name: rel_new_data[rel_opts.pk.attname][0]}) - except ObjectDoesNotExist: - pass - - for f in rel_opts.fields: - if f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) in (None, ''): - all_cores_given = False - elif f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) not in (None, ''): - all_cores_blank = False - # If this field isn't editable, give it the same value it had - # previously, according to the given ID. If the ID wasn't - # given, use a default value. FileFields are also a special - # case, because they'll be dealt with later. - if change and (isinstance(f, FileField) or not f.editable): - if rel_new_data.get(rel_opts.pk.attname, False) and rel_new_data[rel_opts.pk.attname][0]: - params[f.attname] = getattr(old_rel_obj, f.attname) - else: - params[f.attname] = f.get_default() - elif f == rel_field: - params[f.attname] = getattr(new_object, rel_field.rel.field_name) - elif add and isinstance(f, AutoField): - params[f.attname] = None - else: - params[f.attname] = f.get_manipulator_new_data(rel_new_data, rel=True) - # Related links are a special case, because we have to - # manually set the "content_type_id" and "object_id" fields. - if opts.has_related_links and rel_opts.module_name == 'relatedlinks': - contenttypes_mod = get_module('core', 'contenttypes') - params['content_type_id'] = contenttypes_mod.get_object(package__label__exact=opts.app_label, python_module_name__exact=opts.module_name).id - params['object_id'] = new_object.id - - # Create the related item. - new_rel_obj = rel_opts.get_model_module().Klass(**params) - - # If all the core fields were provided (non-empty), save the item. - if all_cores_given: - new_rel_obj.save() - - # Save any uploaded files. - for f in rel_opts.fields: - if isinstance(f, FileField) and rel_new_data.get(f.attname, False): - f.save_file(rel_new_data, new_rel_obj, change and old_rel_obj or None, old_rel_obj is not None, rel=True) - - # Calculate whether any fields have changed. if change: - if not old_rel_obj: # This object didn't exist before. - self.fields_added.append('%s "%r"' % (rel_opts.verbose_name, new_rel_obj)) + if rel_new_data[related.opts.pk.name][0]: + try: + old_rel_obj = getattr(self.original_object, 'get_%s' % related.get_method_name_part() )(**{'%s__exact' % related.opts.pk.name: rel_new_data[related.opts.pk.attname][0]}) + except ObjectDoesNotExist: + pass + + for f in related.opts.fields: + if f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) in (None, ''): + all_cores_given = False + elif f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) not in (None, ''): + all_cores_blank = False + # If this field isn't editable, give it the same value it had + # previously, according to the given ID. If the ID wasn't + # given, use a default value. FileFields are also a special + # case, because they'll be dealt with later. + + if f == related.field: + param = getattr(new_object, related.field.rel.field_name) + elif add and isinstance(f, AutoField): + param = None + elif change and (isinstance(f, FileField) or not child_follow.get(f.name, None)): + if old_rel_obj: + param = getattr(old_rel_obj, f.column) + else: + param = f.get_default() else: - for f in rel_opts.fields: - if not f.primary_key and f != rel_field and str(getattr(old_rel_obj, f.attname)) != str(getattr(new_rel_obj, f.attname)): - self.fields_changed.append('%s for %s "%r"' % (f.verbose_name, rel_opts.verbose_name, new_rel_obj)) + param = f.get_manipulator_new_data(rel_new_data, rel=True) + if param != None: + params[f.attname] = param - # Save many-to-many objects. - for f in rel_opts.many_to_many: - if not f.rel.edit_inline: - was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.attname]) - if change and was_changed: - self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, rel_opts.verbose_name, new_rel_obj)) - # If, in the change stage, all of the core fields were blank and - # the primary key (ID) was provided, delete the item. - if change and all_cores_blank and rel_new_data.has_key(rel_opts.pk.attname) and rel_new_data[rel_opts.pk.attname][0]: - new_rel_obj.delete() - self.fields_deleted.append('%s "%r"' % (rel_opts.verbose_name, old_rel_obj)) + # Related links are a special case, because we have to + # manually set the "content_type_id" and "object_id" fields. + if opts.has_related_links and related.opts.module_name == 'relatedlinks': + contenttypes_mod = get_module('core', 'contenttypes') + params['content_type_id'] = contenttypes_mod.get_object(package__label__exact=opts.app_label, python_module_name__exact=opts.module_name).id + params['object_id'] = new_object.id + + # Create the related item. + new_rel_obj = related.opts.get_model_module().Klass(**params) + + + + # If all the core fields were provided (non-empty), save the item. + if all_cores_given: + new_rel_obj.save() + + # Save any uploaded files. + for f in related.opts.fields: + if child_follow.get(f.name, None): + if isinstance(f, FileField) and rel_new_data.get(f.name, False): + f.save_file(rel_new_data, new_rel_obj, change and old_rel_obj or None, old_rel_obj is not None, rel=True) + + # Calculate whether any fields have changed. + if change: + if not old_rel_obj: # This object didn't exist before. + self.fields_added.append('%s "%s"' % (related.opts.verbose_name, new_rel_obj)) + else: + for f in related.opts.fields: + if not f.primary_key and f != related.field and str(getattr(old_rel_obj, f.attname)) != str(getattr(new_rel_obj, f.attname)): + self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj)) + + # Save many-to-many objects. + for f in related.opts.many_to_many: + if child_follow.get(f.name, None) and not f.rel.edit_inline: + was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.attname]) + if change and was_changed: + self.fields_changed.append('%s for %s "%s"' % (f.verbose_name, related.opts.verbose_name, new_rel_obj)) + + # If, in the change stage, all of the core fields were blank and + # the primary key (ID) was provided, delete the item. + if change and all_cores_blank and old_rel_obj: + new_rel_obj.delete() + self.fields_deleted.append('%s "%s"' % (related.opts.verbose_name, old_rel_obj)) + # Save the order, if applicable. if change and opts.get_ordered_objects(): @@ -1670,6 +1818,17 @@ def manipulator_save(opts, klass, add, change, self, new_data): getattr(new_object, 'set_%s_order' % rel_opts.object_name.lower())(order) return new_object +def manipulator_get_related_objects(opts, klass, add, change, self): + return opts.get_followed_related_objects(self.follow) + +def manipulator_flatten_data(opts, klass, add, change, self): + new_data = {} + obj = change and self.original_object or None + for f in opts.get_data_holders(self.follow): + fol = self.follow.get(f.name) + new_data.update(f.flatten_data(fol, obj)) + return new_data + def manipulator_validator_unique_together(field_name_list, opts, self, field_data, all_data): from django.utils.text import get_text_list field_list = [opts.get_field(field_name) for field_name in field_name_list] @@ -1678,6 +1837,9 @@ def manipulator_validator_unique_together(field_name_list, opts, self, field_dat else: kwargs = {'%s__iexact' % field_name_list[0]: field_data} for f in field_list[1:]: + # This is really not going to work for fields that have different + # form fields, e.g. DateTime. + # This validation needs to occur after html2python to be effective. field_val = all_data.get(f.attname, None) if field_val is None: # This will be caught by another validator, assuming the field diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 299348e629..c45c7b6e02 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -59,6 +59,24 @@ def manipulator_validator_unique(f, opts, self, field_data, all_data): return raise validators.ValidationError, _("%(optname)s with this %(fieldname)s already exists.") % {'optname': capfirst(opts.verbose_name), 'fieldname': f.verbose_name} +class BoundField(object): + def __init__(self, field, field_mapping, original): + self.field = field + self.original = original + self.form_fields = self.resolve_form_fields(field_mapping) + + def resolve_form_fields(self, field_mapping): + return [field_mapping[name] for name in self.field.get_manipulator_field_names('')] + + def as_field_list(self): + return [self.field] + + def original_value(self): + if self.original: + return self.original.__dict__[self.field.column] + + def __repr__(self): + return "BoundField:(%s, %s)" % (self.field.name, self.form_fields) # A guide to Field parameters: # @@ -185,7 +203,7 @@ class Field(object): if hasattr(self.default, '__get_value__'): return self.default.__get_value__() return self.default - if self.null: + if not self.empty_strings_allowed or self.null: return None return "" @@ -207,28 +225,28 @@ class Field(object): if self.maxlength and not self.choices: # Don't give SelectFields a maxlength parameter. params['maxlength'] = self.maxlength if isinstance(self.rel, ManyToOne): + params['member_name'] = name_prefix + self.attname if self.rel.raw_id_admin: field_objs = self.get_manipulator_field_objs() params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) else: if self.radio_admin: field_objs = [formfields.RadioSelectField] - params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) params['ul_class'] = get_ul_class(self.radio_admin) else: if self.null: field_objs = [formfields.NullSelectField] else: field_objs = [formfields.SelectField] - params['choices'] = self.get_choices() + params['choices'] = self.get_choices_default() elif self.choices: if self.radio_admin: field_objs = [formfields.RadioSelectField] - params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) params['ul_class'] = get_ul_class(self.radio_admin) else: field_objs = [formfields.SelectField] - params['choices'] = self.get_choices() + + params['choices'] = self.get_choices_default() else: field_objs = self.get_manipulator_field_objs() @@ -294,7 +312,37 @@ class Field(object): if self.choices: return first_choice + list(self.choices) rel_obj = self.rel.to - return first_choice + [(getattr(x, rel_obj.pk.attname), repr(x)) for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)] + return first_choice + [(getattr(x, rel_obj.pk.attname), str(x)) + for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)] + + def get_choices_default(self): + if(self.radio_admin): + return self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) + else: + return self.get_choices() + + def _get_val_from_obj(self, obj): + if obj: + return getattr(obj, self.attname) + else: + return self.get_default() + + def flatten_data(self, follow, obj = None): + """ + Returns a dictionary mapping the field's manipulator field names to its + "flattened" string values for the admin view. obj is the instance to + extract the values from. + """ + return {self.attname: self._get_val_from_obj(obj)} + + def get_follow(self, override=None): + if override != None: + return override + else: + return self.editable + + def bind(self, fieldmapping, original, bound_field_class=BoundField): + return bound_field_class(self, fieldmapping, original) class AutoField(Field): empty_strings_allowed = False @@ -335,8 +383,10 @@ class DateField(Field): empty_strings_allowed = False def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add + #HACKs : auto_now_add/auto_now should be done as a default or a pre_save... if auto_now or auto_now_add: kwargs['editable'] = False + kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) def get_db_prep_lookup(self, lookup_type, value): @@ -351,6 +401,13 @@ class DateField(Field): return datetime.datetime.now() return value + # Needed because of horrible auto_now[_add] behaviour wrt. editable + def get_follow(self, override=None): + if override != None: + return override + else: + return self.editable or self.auto_now or self.auto_now_add + def get_db_prep_save(self, value): # Casts dates into string format for entry into database. if value is not None: @@ -360,6 +417,10 @@ class DateField(Field): def get_manipulator_field_objs(self): return [formfields.DateField] + def flatten_data(self, follow, obj = None): + val = self._get_val_from_obj(obj) + return {self.attname: (val is not None and val.strftime("%Y-%m-%d") or '')} + class DateTimeField(DateField): def get_db_prep_save(self, value): # Casts dates into string format for entry into database. @@ -389,6 +450,12 @@ class DateTimeField(DateField): return datetime.datetime.combine(d, t) return self.get_default() + def flatten_data(self,follow, obj = None): + val = self._get_val_from_obj(obj) + date_field, time_field = self.get_manipulator_field_names('') + return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), + time_field: (val is not None and val.strftime("%H:%M:%S") or '')} + class EmailField(Field): def __init__(self, *args, **kwargs): kwargs['maxlength'] = 75 @@ -587,6 +654,10 @@ class TimeField(Field): def get_manipulator_field_objs(self): return [formfields.TimeField] + def flatten_data(self,follow, obj = None): + val = self._get_val_from_obj(obj) + return {self.attname: (val is not None and val.strftime("%H:%M:%S") or '')} + class URLField(Field): def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): if verify_exists: @@ -647,6 +718,24 @@ class ForeignKey(Field): else: return [formfields.IntegerField] + def get_db_prep_save(self,value): + if value == '' or value == None: + return None + else: + return int(value) + + def flatten_data(self, follow, obj = None): + if not obj: + # In required many-to-one fields with only one available choice, + # select that one available choice. Note: We have to check that + # the length of choices is *2*, not 1, because SelectFields always + # have an initial "blank" value. + if not self.blank and not self.rel.raw_id_admin and self.choices: + choice_list = self.get_choices_default() + if len(choice_list) == 2: + return { self.attname : choice_list[1][0] } + return Field.flatten_data(self, follow, obj) + class ManyToManyField(Field): def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name_plural) @@ -662,11 +751,14 @@ 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(include_blank=False) + choices = self.get_choices_default() return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + def get_choices_default(self): + return Field.get_choices(self, include_blank=False) + def get_m2m_db_table(self, original_opts): "Returns the name of the many-to-many 'join' table." return '%s_%s' % (original_opts.db_table, self.name) @@ -688,6 +780,25 @@ class ManyToManyField(Field): 'value': len(badkeys) == 1 and badkeys[0] or tuple(badkeys), } + def flatten_data(self, follow, obj = None): + new_data = {} + if obj: + get_list_func = getattr(obj, 'get_%s_list' % self.rel.singular) + instance_ids = [getattr(instance, self.rel.to.pk.attname) for instance in get_list_func()] + if self.rel.raw_id_admin: + new_data[self.name] = ",".join([str(id) for id in instance_ids]) + else: + new_data[self.name] = instance_ids + else: + # In required many-to-many fields with only one available choice, + # select that one available choice. + if not self.blank and not self.rel.edit_inline and not self.rel.raw_id_admin: + choices_list = self.get_choices_default() + if len(choices_list) == 1: + print self.name, choices_list[0][0] + new_data[self.name] = [choices_list[0][0]] + return new_data + class OneToOneField(IntegerField): def __init__(self, to, to_field=None, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', 'ID') @@ -753,6 +864,66 @@ class OneToOne(ManyToOne): self.lookup_overrides = lookup_overrides or {} self.raw_id_admin = raw_id_admin +class BoundFieldLine(object): + def __init__(self, field_line, field_mapping, original, bound_field_class=BoundField): + self.bound_fields = [field.bind(field_mapping, original, bound_field_class) for field in field_line] + + def __iter__(self): + for bound_field in self.bound_fields: + yield bound_field + + def __len__(self): + return len(self.bound_fields) + +class FieldLine(object): + def __init__(self, field_locator_func, linespec): + if isinstance(linespec, basestring): + self.fields = [field_locator_func(linespec)] + else: + self.fields = [field_locator_func(field_name) for field_name in linespec] + + def bind(self, field_mapping, original, bound_field_line_class=BoundFieldLine): + return bound_field_line_class(self, field_mapping, original) + + def __iter__(self): + for field in self.fields: + yield field + + def __len__(self): + return len(self.fields) + +class BoundFieldSet(object): + def __init__(self, field_set, field_mapping, original, bound_field_line_class=BoundFieldLine): + self.name = field_set.name + self.classes = field_set.classes + self.bound_field_lines = [field_line.bind(field_mapping,original, bound_field_line_class) for field_line in field_set] + + def __iter__(self): + for bound_field_line in self.bound_field_lines: + yield bound_field_line + + def __len__(self): + return len(self.bound_field_lines) + +class FieldSet(object): + def __init__(self, name, classes, field_locator_func, line_specs): + self.name = name + self.field_lines = [FieldLine(field_locator_func, line_spec) for line_spec in line_specs] + self.classes = classes + + def __repr__(self): + return "FieldSet:(%s,%s)" % (self.name, self.field_lines) + + def bind(self, field_mapping, original, bound_field_set_class=BoundFieldSet): + return bound_field_set_class(self, field_mapping, original) + + def __iter__(self): + for field_line in self.field_lines: + yield field_line + + def __len__(self): + return len(self.field_lines) + class Admin: def __init__(self, fields=None, js=None, list_display=None, list_filter=None, date_hierarchy=None, save_as=False, ordering=None, search_fields=None, save_on_top=False, list_select_related=False): @@ -766,26 +937,18 @@ class Admin: self.save_on_top = save_on_top self.list_select_related = list_select_related - def get_field_objs(self, opts): - """ - Returns self.fields, except with fields as Field objects instead of - field names. If self.fields is None, defaults to putting every - non-AutoField field with editable=True in a single fieldset. - """ + def get_field_sets(self, opts): if self.fields is None: - field_struct = ((None, {'fields': [f.name for f in opts.fields + opts.many_to_many if f.editable and not isinstance(f, AutoField)]}),) + field_struct = ((None, { + 'fields': [f.name for f in opts.fields + opts.many_to_many if f.editable and not isinstance(f, AutoField)] + }),) else: field_struct = self.fields new_fieldset_list = [] for fieldset in field_struct: - new_fieldset = [fieldset[0], {}] - new_fieldset[1].update(fieldset[1]) - admin_fields = [] - for field_name_or_list in fieldset[1]['fields']: - if isinstance(field_name_or_list, basestring): - admin_fields.append([opts.get_field(field_name_or_list)]) - else: - admin_fields.append([opts.get_field(field_name) for field_name in field_name_or_list]) - new_fieldset[1]['fields'] = admin_fields - new_fieldset_list.append(new_fieldset) + name = fieldset[0] + fs_options = fieldset[1] + classes = fs_options.get('classes', ()) + line_specs = fs_options['fields'] + new_fieldset_list.append(FieldSet(name, classes, opts.get_field, line_specs)) return new_fieldset_list diff --git a/django/models/__init__.py b/django/models/__init__.py index 0796cb5c68..ceb260574b 100644 --- a/django/models/__init__.py +++ b/django/models/__init__.py @@ -19,58 +19,61 @@ for mod in modules: # 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(): + for related in klass._meta.get_all_related_objects(): # Determine whether this related object is in another app. # If it's in another app, the method names will have the app # label prepended, and the add_BLAH() method will not be # generated. - rel_mod = 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): + rel_mod = related.opts.get_model_module() + 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() - func = 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) + func = curry(meta.method_get_related, 'get_object', rel_mod, related.field) + func.__doc__ = "Returns the associated `%s.%s` object." % (related.opts.app_label, related.opts.module_name) setattr(klass, 'get_%s' % rel_obj_name, func) - elif isinstance(rel_field.rel, meta.ManyToOne): + elif isinstance(related.field.rel, meta.ManyToOne): # Add "get_thingie" methods for many-to-one related objects. # EXAMPLE: Poll.get_choice() - func = 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) + func = curry(meta.method_get_related, 'get_object', rel_mod, related.field) + func.__doc__ = "Returns the associated `%s.%s` object matching the given criteria." % \ + (related.opts.app_label, related.opts.module_name) setattr(klass, 'get_%s' % rel_obj_name, func) # Add "get_thingie_count" methods for many-to-one related objects. # EXAMPLE: Poll.get_choice_count() - func = curry(meta.method_get_related, 'get_count', rel_mod, rel_field) - func.__doc__ = "Returns the number of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.module_name) + func = curry(meta.method_get_related, 'get_count', rel_mod, related.field) + func.__doc__ = "Returns the number of associated `%s.%s` objects." % \ + (related.opts.app_label, related.opts.module_name) setattr(klass, 'get_%s_count' % rel_obj_name, func) # Add "get_thingie_list" methods for many-to-one related objects. # EXAMPLE: Poll.get_choice_list() - func = curry(meta.method_get_related, 'get_list', rel_mod, rel_field) - func.__doc__ = "Returns a list of associated `%s.%s` objects." % (rel_obj.app_label, rel_obj.module_name) + func = curry(meta.method_get_related, 'get_list', rel_mod, related.field) + func.__doc__ = "Returns a list of associated `%s.%s` objects." % \ + (related.opts.app_label, related.opts.module_name) setattr(klass, 'get_%s_list' % rel_obj_name, func) # Add "add_thingie" methods for many-to-one related objects, # but only for related objects that are in the same app. # EXAMPLE: Poll.add_choice() - if rel_obj.app_label == klass._meta.app_label: - func = curry(meta.method_add_related, rel_obj, rel_mod, rel_field) + if related.opts.app_label == klass._meta.app_label: + func = curry(meta.method_add_related, related.opts, rel_mod, related.field) func.alters_data = True setattr(klass, 'add_%s' % rel_obj_name, func) del func - del rel_obj_name, rel_mod, rel_obj, rel_field # clean up + 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/views/generic/create_update.py b/django/views/generic/create_update.py index f06e1d57e5..af853a9f4e 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -9,7 +9,7 @@ from django.core.exceptions import Http404, ObjectDoesNotExist, ImproperlyConfig def create_object(request, app_label, module_name, template_name=None, template_loader=template_loader, extra_context={}, - post_save_redirect=None, login_required=False): + post_save_redirect=None, login_required=False, follow=None): """ Generic object-creation function. @@ -22,17 +22,17 @@ def create_object(request, app_label, module_name, template_name=None, return redirect_to_login(request.path) mod = models.get_module(app_label, module_name) - manipulator = mod.AddManipulator() + manipulator = mod.AddManipulator(follow=follow) if request.POST: # If data was POSTed, we're trying to create a new object new_data = request.POST.copy() # Check for errors errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) if not errors: # No errors -- this means we can save the data! - manipulator.do_html2python(new_data) new_object = manipulator.save(new_data) if not request.user.is_anonymous(): @@ -48,7 +48,8 @@ def create_object(request, app_label, module_name, template_name=None, raise ImproperlyConfigured("No URL to redirect to from generic create view.") else: # No POST, so we want a brand new form without any data or errors - errors = new_data = {} + errors = {} + new_data = manipulator.flatten_data() # Create the FormWrapper, template, context, response form = formfields.FormWrapper(manipulator, new_data, errors) @@ -68,7 +69,7 @@ def create_object(request, app_label, module_name, template_name=None, def update_object(request, app_label, module_name, object_id=None, slug=None, slug_field=None, template_name=None, template_loader=template_loader, extra_lookup_kwargs={}, extra_context={}, post_save_redirect=None, - login_required=False): + login_required=False, follow=None): """ Generic object-update function. @@ -98,13 +99,13 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, except ObjectDoesNotExist: raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) - manipulator = mod.ChangeManipulator(object.id) + manipulator = mod.ChangeManipulator(object.id, follow=follow) if request.POST: new_data = request.POST.copy() errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) if not errors: - manipulator.do_html2python(new_data) manipulator.save(new_data) if not request.user.is_anonymous(): @@ -120,7 +121,7 @@ def update_object(request, app_label, module_name, object_id=None, slug=None, else: errors = {} # This makes sure the form acurate represents the fields of the place. - new_data = object.__dict__ + new_data = manipulator.flatten_data() form = formfields.FormWrapper(manipulator, new_data, errors) if not template_name: diff --git a/setup.py b/setup.py index 6374d7f394..6ddfd7a894 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ setup( 'django.contrib.admin': ['templates/admin/*.html', 'templates/admin_doc/*.html', 'templates/registration/*.html', + 'templates/widget/*.html', 'media/css/*.css', 'media/img/admin/*.gif', 'media/img/admin/*.png',