1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

newforms-admin: Got StackedInline and TabularInline working. The templates still need work, and tests for FormSet-Model integration are needed.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@5473 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Joseph Kocherhans 2007-06-15 03:40:45 +00:00
parent fe5194ecfd
commit ec622fc2a9
8 changed files with 366 additions and 15 deletions

View File

@ -1,2 +1,3 @@
from django.contrib.admin.options import ModelAdmin
from django.contrib.admin.options import StackedInline, TabularInline
from django.contrib.admin.sites import AdminSite, site

View File

@ -1,5 +1,7 @@
from django import oldforms, template
from django import newforms as forms
from django.newforms.formsets import all_valid
from django.newforms.models import inline_formset
from django.contrib.admin import widgets
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.db import models
@ -94,6 +96,77 @@ class BoundField(object):
attrs = classes and {'class': ' '.join(classes)} or {}
return self.field.label_tag(contents=contents, attrs=attrs)
class InlineOptions(object):
"""
Options for inline editing of ``model`` instances.
Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
``model`` to its parent. This is required if ``model`` has more than one
``ForeignKey`` to its parent.
"""
def __init__(self, model, name=None, extra=3, fields=None, template=None, formfield_callback=lambda f: f.formfield()):
self.model = model
self.name = name
self.extra = extra
self.fields = fields
self.template = template or self.default_template
self.verbose_name = model._meta.verbose_name
self.verbose_name_plural = model._meta.verbose_name_plural
self.prepopulated_fields = {}
self.formfield_callback = formfield_callback
class StackedInline(InlineOptions):
default_template = 'admin/edit_inline_stacked.html'
class TabularInline(InlineOptions):
default_template = 'admin/edit_inline_tabular.html'
class BoundInline(object):
def __init__(self, opts, formset):
self.opts = opts
self.formset = formset
def __iter__(self):
for form, original in zip(self.formset.change_forms, self.formset.get_inline_objects()):
yield BoundInlineObject(form, original, self.opts)
for form in self.formset.add_forms:
yield BoundInlineObject(form, None, self.opts)
def fields(self):
# HACK: each form instance has some extra fields. Getting those fields
# from the form class will take some rearranging. Get them from the
# first form instance for now.
return list(self.formset.forms[0])
def verbose_name(self):
return self.opts.verbose_name
def verbose_name_plural(self):
return self.opts.verbose_name_plural
class BoundInlineObject(object):
def __init__(self, form, original, opts):
self.opts = opts
self.base_form = form
self.form = AdminForm(form, self.fieldsets(), opts.prepopulated_fields)
self.original = original
def fieldsets(self):
"""
Generator that yields Fieldset objects for use on add and change admin
form pages.
This default implementation looks at self.fields, but subclasses can
override this implementation and do something special based on the
given HttpRequest object.
"""
if self.opts.fields is None:
default_fields = [f for f in self.base_form.fields]
yield Fieldset(fields=default_fields)
else:
for name, options in self.opts.fields:
yield Fieldset(name, options['fields'], classes=options.get('classes', '').split(' '), description=options.get('description'))
class ModelAdmin(object):
"Encapsulates all admin options and functionality for a given model."
@ -292,7 +365,7 @@ class ModelAdmin(object):
"""
return self.queryset(request)
def save_add(self, request, model, form, post_url_continue):
def save_add(self, request, model, form, formsets, post_url_continue):
"""
Saves the object in the "add" stage and returns an HttpResponseRedirect.
@ -302,6 +375,14 @@ class ModelAdmin(object):
from django.contrib.contenttypes.models import ContentType
opts = model._meta
new_object = form.save(commit=True)
if formsets:
for formset in formsets:
# HACK: it seems like the parent obejct should be passed into
# a method of something, not just set as an attribute
formset.instance = new_object
formset.save()
pk_value = new_object._get_pk_val()
LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, str(new_object), ADDITION)
msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object}
@ -331,11 +412,13 @@ class ModelAdmin(object):
post_url = '../../../'
return HttpResponseRedirect(post_url)
def save_change(self, request, model, form):
def save_change(self, request, model, form, formsets=None):
"""
Saves the object in the "change" stage and returns an HttpResponseRedirect.
`form` is a bound Form instance that's verified to be valid.
`formsets` is a sequence of InlineFormSet instances that are verified to be valid.
"""
from django.contrib.admin.models import LogEntry, CHANGE
from django.contrib.contenttypes.models import ContentType
@ -343,6 +426,10 @@ class ModelAdmin(object):
new_object = form.save(commit=True)
pk_value = new_object._get_pk_val()
if formsets:
for formset in formsets:
formset.save()
# Construct the change message. TODO: Temporarily commented-out,
# as manipulator object doesn't exist anymore, and we don't yet
# have a way to get fields_added, fields_changed, fields_deleted.
@ -394,15 +481,22 @@ class ModelAdmin(object):
ModelForm = forms.form_for_model(model, formfield_callback=self.formfield_for_dbfield)
inline_formsets = []
if request.POST:
new_data = request.POST.copy()
if opts.has_field_type(models.FileField):
new_data.update(request.FILES)
form = ModelForm(new_data)
if form.is_valid():
return self.save_add(request, model, form, '../%s/')
for FormSet in self.get_inline_formsets():
inline_formset = FormSet(data=new_data)
inline_formsets.append(inline_formset)
if form.is_valid() and all_valid(inline_formsets):
return self.save_add(request, model, form, inline_formsets, '../%s/')
else:
form = ModelForm(initial=request.GET)
for FormSet in self.get_inline_formsets():
inline_formset = FormSet()
inline_formsets.append(inline_formset)
c = template.RequestContext(request, {
'title': _('Add %s') % opts.verbose_name,
@ -410,6 +504,7 @@ class ModelAdmin(object):
'is_popup': request.REQUEST.has_key('_popup'),
'show_delete': False,
'javascript_imports': self.javascript_add(request),
'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)],
})
return render_change_form(self, model, model.AddManipulator(), c, add=True)
@ -439,16 +534,23 @@ class ModelAdmin(object):
ModelForm = forms.form_for_instance(obj, formfield_callback=self.formfield_for_dbfield)
inline_formsets = []
if request.POST:
new_data = request.POST.copy()
if opts.has_field_type(models.FileField):
new_data.update(request.FILES)
form = ModelForm(new_data)
for FormSet in self.get_inline_formsets():
inline_formset = FormSet(obj, new_data)
inline_formsets.append(inline_formset)
if form.is_valid():
return self.save_change(request, model, form)
if form.is_valid() and all_valid(inline_formsets):
return self.save_change(request, model, form, inline_formsets)
else:
form = ModelForm()
for FormSet in self.get_inline_formsets():
inline_formset = FormSet(obj)
inline_formsets.append(inline_formset)
## Populate the FormWrapper.
#oldform = oldforms.FormWrapper(manipulator, new_data, errors)
@ -463,7 +565,6 @@ class ModelAdmin(object):
#related.get_accessor_name())
#orig_list = func()
#oldform.order_objects.extend(orig_list)
c = template.RequestContext(request, {
'title': _('Change %s') % opts.verbose_name,
'adminform': AdminForm(form, self.fieldsets_change(request, obj), self.prepopulated_fields),
@ -471,6 +572,7 @@ class ModelAdmin(object):
'original': obj,
'is_popup': request.REQUEST.has_key('_popup'),
'javascript_imports': self.javascript_change(request, obj),
'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)],
})
return render_change_form(self, model, model.ChangeManipulator(object_id), c, change=True)
@ -572,3 +674,10 @@ class ModelAdmin(object):
"admin/object_history.html"
]
return render_to_response(template_list, extra_context, context_instance=template.RequestContext(request))
def get_inline_formsets(self):
inline_formset_classes = []
for opts in self.inlines:
inline = inline_formset(self.model, opts.model, formfield_callback=self.formfield_for_dbfield, fields=opts.fields, extra=opts.extra)
inline_formset_classes.append(inline)
return inline_formset_classes

View File

@ -63,6 +63,10 @@
{% block after_field_sets %}{% endblock %}
{% for bound_inline in bound_inlines %}
{% render_inline bound_inline %}
{% endfor %}
{% block after_related_objects %}{% endblock %}
{% submit_row %}

View File

@ -1,3 +1,32 @@
{{ bound_inline.formset.management_form }}
{% for bound_inline_object in bound_inline %}
<fieldset class="module aligned {{ bfset.fieldset.classes }}">
<h2>{{ bound_inline.verbose_name|title }}&nbsp;#{{ forloop.counter }}</h2>
{% for bfset in bound_inline_object.form %}
<fieldset class="module aligned {{ bfset.fieldset.classes }}">
{% if bfset.fieldset.name %}<h2>{{ bfset.fieldset.name }}</h2>{% endif %}
{% if bfset.fieldset.description %}<div class="description">{{ bfset.fieldset.description }}</div>{% endif %}
{% for line in bfset %}
<div class="form-row{% if line.errors %} errors{% endif %}">
{{ line.errors }}
{% for field in line %}
{% if field.is_checkbox %}
{{ field.field }}{{ field.label_tag }}
{% else %}
{{ field.label_tag }}{{ field.field }}
{% endif %}
{% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text }}</p>{% endif %}
{% endfor %}
</div>
{% endfor %}
</fieldset>
{% endfor %}
</fieldset>
{% endfor %}
{% comment %}
<!-- Old forms. Here for reference until new forms match features -->
{% load admin_modify %}
<fieldset class="module aligned">
{% for fcw in bound_related_object.form_field_collection_wrappers %}
@ -14,3 +43,4 @@
{% endfor %}
{% endfor %}
</fieldset>
{% endcomment %}

View File

@ -1,3 +1,53 @@
{{ bound_inline.formset.management_form }}
<fieldset class="module">
<h2>{{ bound_inline.verbose_name_plural|capfirst|escape }}</h2>
<table>
<thead><tr>
{% for field in bound_inline.fields %}
{% if not field.is_hidden %}
<th>{{ field.label|capfirst|escape }}</th>
{% endif %}
{% endfor %}
</tr></thead>
{% for bound_inline_object in bound_inline %}
<!-- still need optional original object -->
{% if bound_inline_object.form.form.errors %}
<tr class="errorlist"><td colspan="{{ bound_inline.fields|length }}">
{{ bound_inline_object.form.form.errors }}
</tr>
{% endif %}
<tr class="{% cycle row1,row2 %}">
{% for bfset in bound_inline_object.form %}
{% for line in bfset %}
{% for field in line %}
{% if not field.field.is_hidden %}
<td>{{ field.field }}</td>
{% else %}
{{ field.field }}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
<!-- still need optional view on site link -->
</tr>
{% endfor %}
</table>
<!-- still need for fcw in bound_related_object.form_field_collection_wrappers -->
</fieldset>
{% comment %}
<!-- Old forms. Here for reference until new forms match features -->
{% load admin_modify %}
<fieldset class="module">
<h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst|escape }}</h2><table>
@ -42,3 +92,4 @@
{% endfor %}
{% endfor %}
</fieldset>
{% endcomment %}

View File

@ -104,3 +104,20 @@ def field_widget(parser, token):
raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0]
return FieldWidgetNode(bits[1])
field_widget = register.tag(field_widget)
class InlineNode(template.Node):
def __init__(self, inline_var):
self.inline_var = inline_var
def render(self, context):
inline = context[self.inline_var]
t = loader.get_template(inline.opts.template)
output = t.render(context)
return output
def render_inline(parser, token):
bits = token.contents.split()
if len(bits) != 2:
raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0]
return InlineNode(bits[1])
render_inline = register.tag(render_inline)

View File

@ -1,18 +1,20 @@
from django import newforms as forms
from forms import Form, ValidationError
from fields import IntegerField, BooleanField
from widgets import HiddenInput
# special field names
FORM_COUNT_FIELD_NAME = 'COUNT'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'
class ManagementForm(forms.Form):
class ManagementForm(Form):
"""
``ManagementForm`` is used to keep track of how many form instances
are displayed on the page. If adding new forms via javascript, you should
increment the count field of this form as well.
"""
def __init__(self, *args, **kwargs):
self.base_fields[FORM_COUNT_FIELD_NAME] = forms.IntegerField(widget=forms.HiddenInput)
self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
super(ManagementForm, self).__init__(*args, **kwargs)
class BaseFormSet(object):
@ -33,7 +35,7 @@ class BaseFormSet(object):
self.change_form_count = self.total_forms - self.num_extra
else:
# not sure that ValidationError is the best thing to raise here
raise forms.ValidationError('ManagementForm data is missing or has been tampered with')
raise ValidationError('ManagementForm data is missing or has been tampered with')
elif initial:
self.change_form_count = len(initial)
self.required_forms = len(initial)
@ -136,9 +138,9 @@ class BaseFormSet(object):
def add_fields(self, form, index):
"""A hook for adding extra fields on to each form instance."""
if self.orderable:
form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1)
form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
if self.deletable:
form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False)
form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
def add_prefix(self, index):
return '%s-%s' % (self.prefix, index)
@ -151,3 +153,10 @@ def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, de
"""Return a FormSet for the given form class."""
attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
return type(form.__name__ + 'FormSet', (formset,), attrs)
def all_valid(formsets):
"""Returns true if every formset in formsets is valid."""
for formset in formsets:
if not formset.is_valid():
return False
return True

View File

@ -7,8 +7,9 @@ from django.utils.translation import gettext
from util import ValidationError
from forms import BaseForm, SortedDictFromList
from fields import Field, ChoiceField
from widgets import Select, SelectMultiple, MultipleHiddenInput
from fields import Field, ChoiceField, IntegerField
from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
__all__ = (
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
@ -197,3 +198,132 @@ class ModelMultipleChoiceField(ModelChoiceField):
else:
final_values.append(obj)
return final_values
# Model-FormSet integration ###################################################
def initial_data(instance, fields=None):
"""
Return a dictionary from data in ``instance`` that is suitable for
use as a ``Form`` constructor's ``initial`` argument.
Provide ``fields`` to specify the names of specific fields to return.
All field values in the instance will be returned if ``fields`` is not
provided.
"""
model = instance.__class__
opts = model._meta
initial = {}
for f in opts.fields + opts.many_to_many:
if not f.editable:
continue
if fields and not f.name in fields:
continue
initial[f.name] = f.value_from_object(instance)
return initial
class BaseModelFormSet(BaseFormSet):
"""
A ``FormSet`` attatched to a particular model or sequence of model instances.
"""
model = None
def __init__(self, data=None, auto_id='id_%s', prefix=None, instances=None):
self.instances = instances
kwargs = {'data': data, 'auto_id': auto_id, 'prefix': prefix}
if instances:
kwargs['initial'] = [initial_data(instance) for instance in instances]
super(BaseModelFormSet, self).__init__(**kwargs)
def save_new(self, form, commit=True):
"""Saves and retrutns a new model instance for the given form."""
return save_instance(form, self.model(), commit=commit)
def save_instance(self, form, instance, commit=True):
"""Saves and retrutns an existing model instance for the given form."""
return save_instance(form, instance, commit=commit)
def save(self, commit=True):
"""Saves model instances for every form, adding and changing instances
as necessary, and returns the list of instances.
"""
saved_instances = []
# put self.instances into a dict so they are easy to lookup by pk
instances = {}
for instance in self.instances:
instances[instance._get_pk_val()] = instance
if self.instances:
# update/save existing instances
for form in self.change_forms:
instance = instances[form.cleaned_data[self.model._meta.pk.attname]]
if form.cleaned_data[DELETION_FIELD_NAME]:
instance.delete()
else:
saved_instances.append(self.save_instance(form, instance, commit=commit))
# create/save new instances
for form in self.add_forms:
if form.is_empty():
continue
saved_instances.append(self.save_new(form, commit=commit))
return saved_instances
def add_fields(self, form, index):
"""Add a hidden field for the object's primary key."""
form.fields[self.model._meta.pk.attname] = IntegerField(required=False, widget=HiddenInput)
super(BaseModelFormSet, self).add_fields(form, index)
def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
FormSet = formset_for_form(form, formset, extra, orderable, deletable)
FormSet.model = model
return FormSet
class InlineFormset(BaseModelFormSet):
"""A formset for child objects related to a parent."""
def __init__(self, instance=None, data=None):
self.instance = instance
super(InlineFormset, self).__init__(data, instances=self.get_inline_objects())
def get_inline_objects(self):
if self.instance is None:
return []
from django.db.models.fields.related import RelatedObject
# is there a better way to get the object descriptor?
rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
return getattr(self.instance, rel_name).all()
def save_new(self, form, commit=True):
kwargs = {self.fk.get_attname(): self.instance._get_pk_val()}
new_obj = self.model(**kwargs)
return save_instance(form, new_obj, commit=commit)
def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, formfield_callback=lambda f: f.formfield()):
"""
Returns an ``InlineFormset`` for the given kwargs.
You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
to ``parent_model``.
"""
from django.db.models import ForeignKey
opts = model._meta
# figure out what the ForeignKey from model to parent_model is
if fk_name is None:
fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
if len(fks_to_parent) == 1:
fk = fks_to_parent[0]
elif len(fks_to_parent) == 0:
raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
else:
raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
# let the formset handle object deletion by default
FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, formfield_callback=formfield_callback, extra=extra, deletable=True)
# HACK: remove the ForeignKey to the parent from every form
# This should be done a line above before we pass 'fields' to formset_for_model
# an 'omit' argument would be very handy here
try:
del FormSet.form_class.base_fields[fk.name]
except KeyError:
pass
FormSet.parent_model = parent_model
FormSet.fk_name = fk.name
FormSet.fk = fk
return FormSet