Django

Code

Changeset 5473

Show
Ignore:
Timestamp:
06/14/07 22:40:45 (2 years ago)
Author:
jkocherhans
Message:

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

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/newforms-admin/django/contrib/admin/__init__.py

    r4581 r5473  
    11from django.contrib.admin.options import ModelAdmin 
     2from django.contrib.admin.options import StackedInline, TabularInline 
    23from django.contrib.admin.sites import AdminSite, site 
  • django/branches/newforms-admin/django/contrib/admin/options.py

    r4941 r5473  
    11from django import oldforms, template 
    22from django import newforms as forms 
     3from django.newforms.formsets import all_valid 
     4from django.newforms.models import inline_formset 
    35from django.contrib.admin import widgets 
    46from django.core.exceptions import ImproperlyConfigured, PermissionDenied 
     
    9496        attrs = classes and {'class': ' '.join(classes)} or {} 
    9597        return self.field.label_tag(contents=contents, attrs=attrs) 
     98 
     99class InlineOptions(object): 
     100    """ 
     101    Options for inline editing of ``model`` instances. 
     102     
     103    Provide ``name`` to specify the attribute name of the ``ForeignKey`` from 
     104    ``model`` to its parent. This is required if ``model`` has more than one 
     105    ``ForeignKey`` to its parent. 
     106    """ 
     107    def __init__(self, model, name=None, extra=3, fields=None, template=None, formfield_callback=lambda f: f.formfield()): 
     108        self.model = model 
     109        self.name = name 
     110        self.extra = extra 
     111        self.fields = fields 
     112        self.template = template or self.default_template 
     113        self.verbose_name = model._meta.verbose_name 
     114        self.verbose_name_plural = model._meta.verbose_name_plural 
     115        self.prepopulated_fields = {} 
     116        self.formfield_callback = formfield_callback 
     117 
     118class StackedInline(InlineOptions): 
     119    default_template = 'admin/edit_inline_stacked.html' 
     120 
     121class TabularInline(InlineOptions): 
     122    default_template = 'admin/edit_inline_tabular.html' 
     123 
     124class BoundInline(object): 
     125    def __init__(self, opts, formset): 
     126        self.opts = opts 
     127        self.formset = formset 
     128 
     129    def __iter__(self): 
     130        for form, original in zip(self.formset.change_forms, self.formset.get_inline_objects()): 
     131            yield BoundInlineObject(form, original, self.opts) 
     132        for form in self.formset.add_forms: 
     133            yield BoundInlineObject(form, None, self.opts) 
     134 
     135    def fields(self): 
     136        # HACK: each form instance has some extra fields. Getting those fields 
     137        # from the form class will take some rearranging. Get them from the  
     138        # first form instance for now. 
     139        return list(self.formset.forms[0]) 
     140 
     141    def verbose_name(self): 
     142        return self.opts.verbose_name 
     143 
     144    def verbose_name_plural(self): 
     145        return self.opts.verbose_name_plural 
     146 
     147class BoundInlineObject(object): 
     148    def __init__(self, form, original, opts): 
     149        self.opts = opts 
     150        self.base_form = form 
     151        self.form = AdminForm(form, self.fieldsets(), opts.prepopulated_fields) 
     152        self.original = original 
     153 
     154    def fieldsets(self): 
     155        """ 
     156        Generator that yields Fieldset objects for use on add and change admin 
     157        form pages. 
     158 
     159        This default implementation looks at self.fields, but subclasses can 
     160        override this implementation and do something special based on the 
     161        given HttpRequest object. 
     162        """ 
     163        if self.opts.fields is None: 
     164            default_fields = [f for f in self.base_form.fields] 
     165            yield Fieldset(fields=default_fields) 
     166        else: 
     167            for name, options in self.opts.fields: 
     168                yield Fieldset(name, options['fields'], classes=options.get('classes', '').split(' '), description=options.get('description')) 
    96169 
    97170class ModelAdmin(object): 
     
    293366        return self.queryset(request) 
    294367 
    295     def save_add(self, request, model, form, post_url_continue): 
     368    def save_add(self, request, model, form, formsets, post_url_continue): 
    296369        """ 
    297370        Saves the object in the "add" stage and returns an HttpResponseRedirect. 
     
    303376        opts = model._meta 
    304377        new_object = form.save(commit=True) 
     378 
     379        if formsets: 
     380            for formset in formsets: 
     381                # HACK: it seems like the parent obejct should be passed into 
     382                # a method of something, not just set as an attribute 
     383                formset.instance = new_object 
     384                formset.save() 
     385 
    305386        pk_value = new_object._get_pk_val() 
    306387        LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, str(new_object), ADDITION) 
     
    332413            return HttpResponseRedirect(post_url) 
    333414 
    334     def save_change(self, request, model, form): 
     415    def save_change(self, request, model, form, formsets=None): 
    335416        """ 
    336417        Saves the object in the "change" stage and returns an HttpResponseRedirect. 
    337418 
    338419        `form` is a bound Form instance that's verified to be valid. 
     420         
     421        `formsets` is a sequence of InlineFormSet instances that are verified to be valid. 
    339422        """ 
    340423        from django.contrib.admin.models import LogEntry, CHANGE 
     
    343426        new_object = form.save(commit=True) 
    344427        pk_value = new_object._get_pk_val() 
     428 
     429        if formsets: 
     430            for formset in formsets: 
     431                formset.save() 
    345432 
    346433        # Construct the change message. TODO: Temporarily commented-out, 
     
    395482        ModelForm = forms.form_for_model(model, formfield_callback=self.formfield_for_dbfield) 
    396483 
     484        inline_formsets = [] 
    397485        if request.POST: 
    398486            new_data = request.POST.copy() 
     
    400488                new_data.update(request.FILES) 
    401489            form = ModelForm(new_data) 
    402             if form.is_valid(): 
    403                 return self.save_add(request, model, form, '../%s/') 
     490            for FormSet in self.get_inline_formsets(): 
     491                inline_formset = FormSet(data=new_data) 
     492                inline_formsets.append(inline_formset) 
     493            if form.is_valid() and all_valid(inline_formsets): 
     494                return self.save_add(request, model, form, inline_formsets, '../%s/') 
    404495        else: 
    405496            form = ModelForm(initial=request.GET) 
     497            for FormSet in self.get_inline_formsets(): 
     498                inline_formset = FormSet() 
     499                inline_formsets.append(inline_formset) 
    406500 
    407501        c = template.RequestContext(request, { 
     
    411505            'show_delete': False, 
    412506            'javascript_imports': self.javascript_add(request), 
     507            'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)], 
    413508        }) 
    414509        return render_change_form(self, model, model.AddManipulator(), c, add=True) 
     
    440535        ModelForm = forms.form_for_instance(obj, formfield_callback=self.formfield_for_dbfield) 
    441536 
     537        inline_formsets = [] 
    442538        if request.POST: 
    443539            new_data = request.POST.copy() 
     
    445541                new_data.update(request.FILES) 
    446542            form = ModelForm(new_data) 
    447  
    448             if form.is_valid(): 
    449                 return self.save_change(request, model, form) 
     543            for FormSet in self.get_inline_formsets(): 
     544                inline_formset = FormSet(obj, new_data) 
     545                inline_formsets.append(inline_formset) 
     546 
     547            if form.is_valid() and all_valid(inline_formsets): 
     548                return self.save_change(request, model, form, inline_formsets) 
    450549        else: 
    451550            form = ModelForm() 
     551            for FormSet in self.get_inline_formsets(): 
     552                inline_formset = FormSet(obj) 
     553                inline_formsets.append(inline_formset) 
    452554 
    453555        ## Populate the FormWrapper. 
     
    464566                #orig_list = func() 
    465567                #oldform.order_objects.extend(orig_list) 
    466  
    467568        c = template.RequestContext(request, { 
    468569            'title': _('Change %s') % opts.verbose_name, 
     
    472573            'is_popup': request.REQUEST.has_key('_popup'), 
    473574            'javascript_imports': self.javascript_change(request, obj), 
     575            'bound_inlines': [BoundInline(i, fs) for i, fs in zip(self.inlines, inline_formsets)], 
    474576        }) 
    475577        return render_change_form(self, model, model.ChangeManipulator(object_id), c, change=True) 
     
    573675        ] 
    574676        return render_to_response(template_list, extra_context, context_instance=template.RequestContext(request)) 
     677 
     678    def get_inline_formsets(self): 
     679        inline_formset_classes = [] 
     680        for opts in self.inlines: 
     681            inline = inline_formset(self.model, opts.model, formfield_callback=self.formfield_for_dbfield, fields=opts.fields, extra=opts.extra) 
     682            inline_formset_classes.append(inline) 
     683        return inline_formset_classes 
  • django/branches/newforms-admin/django/contrib/admin/templates/admin/change_form.html

    r4569 r5473  
    6464{% block after_field_sets %}{% endblock %} 
    6565 
     66{% for bound_inline in bound_inlines %} 
     67    {% render_inline bound_inline %} 
     68{% endfor %} 
     69 
    6670{% block after_related_objects %}{% endblock %} 
    6771 
  • django/branches/newforms-admin/django/contrib/admin/templates/admin/edit_inline_stacked.html

    r3349 r5473  
     1{{ bound_inline.formset.management_form }} 
     2{% for bound_inline_object in bound_inline %} 
     3<fieldset class="module aligned {{ bfset.fieldset.classes }}"> 
     4  <h2>{{ bound_inline.verbose_name|title }}&nbsp;#{{ forloop.counter }}</h2> 
     5  {% for bfset in bound_inline_object.form %} 
     6      <fieldset class="module aligned {{ bfset.fieldset.classes }}"> 
     7      {% if bfset.fieldset.name %}<h2>{{ bfset.fieldset.name }}</h2>{% endif %} 
     8      {% if bfset.fieldset.description %}<div class="description">{{ bfset.fieldset.description }}</div>{% endif %} 
     9      {% for line in bfset %} 
     10          <div class="form-row{% if line.errors %} errors{% endif %}"> 
     11          {{ line.errors }} 
     12          {% for field in line %} 
     13              {% if field.is_checkbox %} 
     14                  {{ field.field }}{{ field.label_tag }} 
     15              {% else %} 
     16                  {{ field.label_tag }}{{ field.field }} 
     17              {% endif %} 
     18              {% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text }}</p>{% endif %} 
     19          {% endfor %} 
     20          </div> 
     21      {% endfor %} 
     22      </fieldset> 
     23  {% endfor %} 
     24</fieldset> 
     25{% endfor %} 
     26 
     27{% comment %} 
     28<!-- Old forms. Here for reference until new forms match features --> 
     29 
    130{% load admin_modify %} 
    231<fieldset class="module aligned"> 
     
    1544    {% endfor %} 
    1645</fieldset> 
     46{% endcomment %} 
  • django/branches/newforms-admin/django/contrib/admin/templates/admin/edit_inline_tabular.html

    r3571 r5473  
     1{{ bound_inline.formset.management_form }} 
     2<fieldset class="module"> 
     3   <h2>{{ bound_inline.verbose_name_plural|capfirst|escape }}</h2> 
     4   <table> 
     5     <thead><tr> 
     6     {% for field in bound_inline.fields %} 
     7       {% if not field.is_hidden %} 
     8         <th>{{ field.label|capfirst|escape }}</th> 
     9        {% endif %} 
     10     {% endfor %} 
     11     </tr></thead> 
     12    
     13     {% for bound_inline_object in bound_inline %} 
     14    
     15        <!-- still need optional original object --> 
     16         
     17        {% if bound_inline_object.form.form.errors %} 
     18           <tr class="errorlist"><td colspan="{{ bound_inline.fields|length }}"> 
     19              {{ bound_inline_object.form.form.errors }} 
     20           </tr> 
     21        {% endif %} 
     22         
     23        <tr class="{% cycle row1,row2 %}"> 
     24        {% for bfset in bound_inline_object.form %} 
     25          {% for line in bfset %} 
     26            {% for field in line %} 
     27               
     28              {% if not field.field.is_hidden %} 
     29                <td>{{ field.field }}</td> 
     30              {% else %} 
     31                {{ field.field }} 
     32              {% endif %} 
     33            {% endfor %} 
     34          {% endfor %} 
     35        {% endfor %} 
     36         
     37        <!-- still need optional view on site link --> 
     38        </tr> 
     39 
     40     {% endfor %} 
     41    
     42   </table> 
     43 
     44   <!-- still need  for fcw in bound_related_object.form_field_collection_wrappers --> 
     45    
     46</fieldset> 
     47 
     48{% comment %} 
     49<!-- Old forms. Here for reference until new forms match features --> 
     50 
    151{% load admin_modify %} 
    252<fieldset class="module"> 
     
    4393   {% endfor %} 
    4494</fieldset> 
     95{% endcomment %} 
  • django/branches/newforms-admin/django/contrib/admin/templatetags/admin_modify.py

    r5195 r5473  
    105105    return FieldWidgetNode(bits[1]) 
    106106field_widget = register.tag(field_widget) 
     107 
     108class InlineNode(template.Node): 
     109    def __init__(self, inline_var): 
     110        self.inline_var = inline_var 
     111 
     112    def render(self, context): 
     113        inline = context[self.inline_var] 
     114        t = loader.get_template(inline.opts.template) 
     115        output = t.render(context) 
     116        return output 
     117 
     118def render_inline(parser, token): 
     119    bits = token.contents.split() 
     120    if len(bits) != 2: 
     121        raise template.TemplateSyntaxError, "%s takes 1 argument" % bits[0] 
     122    return InlineNode(bits[1]) 
     123render_inline = register.tag(render_inline) 
  • django/branches/newforms-admin/django/newforms/formsets.py

    r5325 r5473  
    1 from django import newforms as forms 
     1from forms import Form, ValidationError 
     2from fields import IntegerField, BooleanField 
     3from widgets import HiddenInput 
    24 
    35# special field names 
     
    68DELETION_FIELD_NAME = 'DELETE' 
    79 
    8 class ManagementForm(forms.Form): 
     10class ManagementForm(Form): 
    911    """ 
    1012    ``ManagementForm`` is used to keep track of how many form instances 
     
    1315    """ 
    1416    def __init__(self, *args, **kwargs): 
    15         self.base_fields[FORM_COUNT_FIELD_NAME] = forms.IntegerField(widget=forms.HiddenInput) 
     17        self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 
    1618        super(ManagementForm, self).__init__(*args, **kwargs) 
    1719 
     
    3436            else: 
    3537                # not sure that ValidationError is the best thing to raise here 
    36                 raise forms.ValidationError('ManagementForm data is missing or has been tampered with') 
     38                raise ValidationError('ManagementForm data is missing or has been tampered with') 
    3739        elif initial: 
    3840            self.change_form_count = len(initial) 
     
    137139        """A hook for adding extra fields on to each form instance.""" 
    138140        if self.orderable: 
    139             form.fields[ORDERING_FIELD_NAME] = forms.IntegerField(label='Order', initial=index+1) 
     141            form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) 
    140142        if self.deletable: 
    141             form.fields[DELETION_FIELD_NAME] = forms.BooleanField(label='Delete', required=False) 
     143            form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False) 
    142144 
    143145    def add_prefix(self, index): 
     
    152154    attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} 
    153155    return type(form.__name__ + 'FormSet', (formset,), attrs) 
     156 
     157def all_valid(formsets): 
     158    """Returns true if every formset in formsets is valid.""" 
     159    for formset in formsets: 
     160        if not formset.is_valid(): 
     161            return False 
     162    return True 
  • django/branches/newforms-admin/django/newforms/models.py

    r5301 r5473  
    88from util import ValidationError 
    99from forms import BaseForm, SortedDictFromList 
    10 from fields import Field, ChoiceField 
    11 from widgets import Select, SelectMultiple, MultipleHiddenInput 
     10from fields import Field, ChoiceField, IntegerField 
     11from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME 
     12from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 
    1213 
    1314__all__ = ( 
     
    198199                final_values.append(obj) 
    199200        return final_values 
     201 
     202# Model-FormSet integration ################################################### 
     203 
     204def initial_data(instance, fields=None): 
     205    """ 
     206    Return a dictionary from data in ``instance`` that is suitable for  
     207    use as a ``Form`` constructor's ``initial`` argument. 
     208     
     209    Provide ``fields`` to specify the names of specific fields to return. 
     210    All field values in the instance will be returned if ``fields`` is not 
     211    provided. 
     212    """ 
     213    model = instance.__class__ 
     214    opts = model._meta 
     215    initial = {} 
     216    for f in opts.fields + opts.many_to_many: 
     217        if not f.editable: 
     218            continue 
     219        if fields and not f.name in fields: 
     220            continue 
     221        initial[f.name] = f.value_from_object(instance) 
     222    return initial 
     223 
     224class BaseModelFormSet(BaseFormSet): 
     225    """ 
     226    A ``FormSet`` attatched to a particular model or sequence of model instances. 
     227    """ 
     228    model = None 
     229     
     230    def __init__(self, data=None, auto_id='id_%s', prefix=None, instances=None): 
     231        self.instances = instances 
     232        kwargs = {'data': data, 'auto_id': auto_id, 'prefix': prefix} 
     233        if instances: 
     234            kwargs['initial'] = [initial_data(instance) for instance in instances] 
     235        super(BaseModelFormSet, self).__init__(**kwargs) 
     236 
     237    def save_new(self, form, commit=True): 
     238        """Saves and retrutns a new model instance for the given form.""" 
     239        return save_instance(form, self.model(), commit=commit) 
     240 
     241    def save_instance(self, form, instance, commit=True): 
     242        """Saves and retrutns an existing model instance for the given form.""" 
     243        return save_instance(form, instance, commit=commit) 
     244 
     245    def save(self, commit=True): 
     246        """Saves model instances for every form, adding and changing instances 
     247        as necessary, and returns the list of instances. 
     248        """ 
     249        saved_instances = [] 
     250        # put self.instances into a dict so they are easy to lookup by pk 
     251        instances = {} 
     252        for instance in self.instances: 
     253            instances[instance._get_pk_val()] = instance 
     254        if self.instances: 
     255            # update/save existing instances 
     256            for form in self.change_forms: 
     257                instance = instances[form.cleaned_data[self.model._meta.pk.attname]] 
     258                if form.cleaned_data[DELETION_FIELD_NAME]: 
     259                    instance.delete() 
     260                else: 
     261                    saved_instances.append(self.save_instance(form, instance, commit=commit)) 
     262        # create/save new instances 
     263        for form in self.add_forms: 
     264            if form.is_empty(): 
     265                continue 
     266            saved_instances.append(self.save_new(form, commit=commit)) 
     267        return saved_instances 
     268 
     269    def add_fields(self, form, index): 
     270        """Add a hidden field for the object's primary key.""" 
     271        form.fields[self.model._meta.pk.attname] = IntegerField(required=False, widget=HiddenInput) 
     272        super(BaseModelFormSet, self).add_fields(form, index) 
     273 
     274def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 
     275    form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 
     276    FormSet = formset_for_form(form, formset, extra, orderable, deletable) 
     277    FormSet.model = model 
     278    return FormSet 
     279 
     280class InlineFormset(BaseModelFormSet): 
     281    """A formset for child objects related to a parent.""" 
     282    def __init__(self, instance=None, data=None): 
     283        self.instance = instance 
     284        super(InlineFormset, self).__init__(data, instances=self.get_inline_objects()) 
     285 
     286    def get_inline_objects(self): 
     287        if self.instance is None: 
     288            return [] 
     289        from django.db.models.fields.related import RelatedObject 
     290        # is there a better way to get the object descriptor? 
     291        rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() 
     292        return getattr(self.instance, rel_name).all() 
     293 
     294    def save_new(self, form, commit=True): 
     295        kwargs = {self.fk.get_attname(): self.instance._get_pk_val()} 
     296        new_obj = self.model(**kwargs) 
     297        return save_instance(form, new_obj, commit=commit) 
     298 
     299def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, formfield_callback=lambda f: f.formfield()): 
     300    """ 
     301    Returns an ``InlineFormset`` for the given kwargs. 
     302     
     303    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey`` 
     304    to ``parent_model``. 
     305    """ 
     306    from django.db.models import ForeignKey 
     307    opts = model._meta 
     308    # figure out what the ForeignKey from model to parent_model is 
     309    if fk_name is None: 
     310        fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model] 
     311        if len(fks_to_parent) == 1: 
     312            fk = fks_to_parent[0] 
     313        elif len(fks_to_parent) == 0: 
     314            raise Exception("%s has no ForeignKey to %s" % (model, parent_model)) 
     315        else: 
     316            raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model)) 
     317    # let the formset handle object deletion by default 
     318    FormSet = formset_for_model(model, formset=InlineFormset, fields=fields, formfield_callback=formfield_callback, extra=extra, deletable=True) 
     319    # HACK: remove the ForeignKey to the parent from every form 
     320    # This should be done a line above before we pass 'fields' to formset_for_model 
     321    # an 'omit' argument would be very handy here 
     322    try: 
     323        del FormSet.form_class.base_fields[fk.name] 
     324    except KeyError: 
     325        pass 
     326    FormSet.parent_model = parent_model 
     327    FormSet.fk_name = fk.name 
     328    FormSet.fk = fk 
     329    return FormSet