Ticket #6241: formset_refactor_6_tests_fail.diff

File formset_refactor_6_tests_fail.diff, 47.5 KB (added by Øyvind Saltvik <oyvind@…>, 16 years ago)

attempt to update patch after merge with trunk

  • django/contrib/admin/options.py

     
    342342            fields = flatten_fieldsets(self.declared_fieldsets)
    343343        else:
    344344            fields = None
    345         return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
     345        return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    346346
    347347    def form_change(self, request, obj):
    348348        """
     
    352352            fields = flatten_fieldsets(self.declared_fieldsets)
    353353        else:
    354354            fields = None
    355         return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield)
     355        return forms.modelform_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield)
    356356
    357357    def save_add(self, request, model, form, formsets, post_url_continue):
    358358        """
     
    492492            # Object list will give 'Permission Denied', so go back to admin home
    493493            post_url = '../../../'
    494494
    495         ModelForm = self.form_add(request)
     495        Form = self.form_add(request)
    496496        inline_formsets = []
    497497        obj = self.model()
    498498        if request.method == 'POST':
    499             form = ModelForm(request.POST, request.FILES)
     499            form = Form(request.POST, request.FILES)
    500500            for FormSet in self.formsets_add(request):
    501                 inline_formset = FormSet(obj, data=request.POST, files=request.FILES)
     501                inline_formset = FormSet(request.POST, request.FILES)
    502502                inline_formsets.append(inline_formset)
    503503            if all_valid(inline_formsets) and form.is_valid():
    504504                return self.save_add(request, model, form, inline_formsets, '../%s/')
    505505        else:
    506             form = ModelForm(initial=request.GET)
     506            form = Form(initial=request.GET)
    507507            for FormSet in self.formsets_add(request):
    508                 inline_formset = FormSet(obj)
     508                inline_formset = FormSet()
    509509                inline_formsets.append(inline_formset)
    510510
    511511        adminForm = AdminForm(form, list(self.fieldsets_add(request)), self.prepopulated_fields)
     
    552552        if request.POST and request.POST.has_key("_saveasnew"):
    553553            return self.add_view(request, form_url='../../add/')
    554554
    555         ModelForm = self.form_change(request, obj)
     555        Form = self.form_change(request, obj)
    556556        inline_formsets = []
    557557        if request.method == 'POST':
    558             form = ModelForm(request.POST, request.FILES)
     558            form = Form(request.POST, request.FILES, instance=obj)
    559559            for FormSet in self.formsets_change(request, obj):
    560                 inline_formset = FormSet(obj, request.POST, request.FILES)
     560                inline_formset = FormSet(request.POST, request.FILES, instance=obj)
    561561                inline_formsets.append(inline_formset)
    562562
    563563            if all_valid(inline_formsets) and form.is_valid():
    564564                return self.save_change(request, model, form, inline_formsets)
    565565        else:
    566             form = ModelForm()
     566            form = Form(instance=obj, initial=request.GET)
    567567            for FormSet in self.formsets_change(request, obj):
    568                 inline_formset = FormSet(obj)
     568                inline_formset = FormSet(instance=obj)
    569569                inline_formsets.append(inline_formset)
    570570
    571571        ## Populate the FormWrapper.
     
    755755    def fieldsets_add(self, request):
    756756        if self.declared_fieldsets:
    757757            return self.declared_fieldsets
    758         form = self.formset_add(request).form_class
    759         return [(None, {'fields': form.base_fields.keys()})]
     758        formset = self.formset_add(request)
     759        return [(None, {'fields': formset.base_fields.keys()})]
    760760
     761
    761762    def fieldsets_change(self, request, obj):
    762763        if self.declared_fieldsets:
    763764            return self.declared_fieldsets
    764         form = self.formset_change(request, obj).form_class
    765         return [(None, {'fields': form.base_fields.keys()})]
     765        formset = self.formset_change(request, obj)
     766        return [(None, {'fields': formset.base_fields.keys()})]
    766767
     768
    767769class StackedInline(InlineModelAdmin):
    768770    template = 'admin/edit_inline/stacked.html'
    769771
     
    778780        self.opts = inline
    779781        self.formset = formset
    780782        self.fieldsets = fieldsets
     783        # place orderable and deletable here since _meta is inaccesible in the
     784        # templates.
     785        self.orderable = formset._meta.orderable
     786        self.deletable = formset._meta.deletable
    781787
    782788    def __iter__(self):
    783789        for form, original in zip(self.formset.change_forms, self.formset.get_queryset()):
     
    787793
    788794    def fields(self):
    789795        for field_name in flatten_fieldsets(self.fieldsets):
    790             yield self.formset.form_class.base_fields[field_name]
     796            yield self.formset.base_fields[field_name]
    791797
    792798class InlineAdminForm(AdminForm):
    793799    """
  • django/contrib/admin/templates/admin/edit_inline/tabular.html

     
    1111         <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst|escape }}</th>
    1212        {% endif %}
    1313     {% endfor %}
    14      {% if inline_admin_formset.formset.deletable %}<th>{% trans "Delete" %}?</th>{% endif %}
     14     {% if inline_admin_formset.deletable %}<th>{% trans "Delete" %}?</th>{% endif %}
    1515     </tr></thead>
    1616   
    1717     {% for inline_admin_form in inline_admin_formset %}
     
    4545          {% endfor %}
    4646        {% endfor %}
    4747               
    48         {% if inline_admin_formset.formset.deletable %}<td class="delete">{{ inline_admin_form.deletion_field.field }}</td>{% endif %}
     48        {% if inline_admin_formset.deletable %}<td class="delete">{{ inline_admin_form.deletion_field.field }}</td>{% endif %}
    4949       
    5050        </tr>
    5151
  • django/newforms/options.py

     
     1
     2from forms import BaseForm
     3
     4class BaseFormOptions(object):
     5    """
     6    The base class for all options that are associated to a form object.
     7    """
     8    def __init__(self, options=None):
     9        self.fields = self._dynamic_attribute(options, "fields")
     10        self.exclude = self._dynamic_attribute(options, "exclude")
     11
     12    def _dynamic_attribute(self, obj, key, default=None):
     13        try:
     14            return getattr(obj, key)
     15        except AttributeError:
     16            try:
     17                return obj[key]
     18            except (TypeError, KeyError):
     19                # key doesnt exist in obj or obj is None
     20                return default
     21
     22class ModelFormOptions(BaseFormOptions):
     23    """
     24    Encapsulates the options on a ModelForm class.
     25    """
     26    def __init__(self, options=None):
     27        self.model = self._dynamic_attribute(options, "model")
     28        super(ModelFormOptions, self).__init__(options)
     29
     30class FormSetOptions(BaseFormOptions):
     31    """
     32    Encapsulates the options on a FormSet class.
     33    """
     34    def __init__(self, options=None):
     35        self.form = self._dynamic_attribute(options, "form", BaseForm)
     36        self.num_extra = self._dynamic_attribute(options, "num_extra", 1)
     37        self.orderable = self._dynamic_attribute(options, "orderable", False)
     38        self.deletable = self._dynamic_attribute(options, "deletable", False)
     39        super(FormSetOptions, self).__init__(options)
     40
     41class ModelFormSetOptions(FormSetOptions, ModelFormOptions):
     42    def __init__(self, options=None):
     43        super(ModelFormSetOptions, self).__init__(options)
     44        self.deletable = True
     45
     46class InlineFormSetOptions(ModelFormSetOptions):
     47    def __init__(self, options=None):
     48        super(InlineFormSetOptions, self).__init__(options)
     49        self.parent_model = self._dynamic_attribute(options, "parent_model")
     50 
     51 No newline at end of file
  • django/newforms/formsets.py

     
    1 from forms import Form
    2 from fields import IntegerField, BooleanField
     1from warnings import warn
     2
     3from django.utils.datastructures import SortedDict
     4from django.utils.translation import ugettext_lazy as _
     5
     6from forms import BaseForm, Form
     7from fields import Field, IntegerField, BooleanField
     8from options import FormSetOptions
     9
    310from widgets import HiddenInput, Media
    411from util import ErrorList, ValidationError
    512
    6 __all__ = ('BaseFormSet', 'formset_for_form', 'all_valid')
     13__all__ = ('BaseFormSet', 'FormSet', 'formset_for_form', 'all_valid')
    714
    815# special field names
    916FORM_COUNT_FIELD_NAME = 'COUNT'
     
    1926    def __init__(self, *args, **kwargs):
    2027        self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput)
    2128        super(ManagementForm, self).__init__(*args, **kwargs)
     29       
     30class BaseFormSetMetaclass(type):
     31    def __new__(cls, name, bases, attrs, **options):
     32        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
     33        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
    2234
     35        # If this class is subclassing another FormSet, ad that FormSet's fields.
     36        # Note that we loop over the bases in *reverse*. This is necessary in
     37        # order to preserve the correct order of fields.
     38        for base in bases[::-1]:
     39            if hasattr(base, "base_fields"):
     40                fields = base.base_fields.items() + fields
     41        attrs["base_fields"] = SortedDict(fields)
     42
     43        opts = FormSetOptions(options and options or attrs.get("Meta", None))
     44        attrs["_meta"] = opts
     45
     46        return type.__new__(cls, name, bases, attrs)
     47
    2348class BaseFormSet(object):
    2449    """A collection of instances of the same Form class."""
    2550
     
    3762            self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix)
    3863            if self.management_form.is_valid():
    3964                self.total_forms = self.management_form.cleaned_data[FORM_COUNT_FIELD_NAME]
    40                 self.required_forms = self.total_forms - self.num_extra
    41                 self.change_form_count = self.total_forms - self.num_extra
     65                self.required_forms = self.total_forms - self._meta.num_extra
     66                self.change_form_count = self.total_forms - self._meta.num_extra
    4267            else:
    4368                # not sure that ValidationError is the best thing to raise here
    4469                raise ValidationError('ManagementForm data is missing or has been tampered with')
    4570        elif initial:
    4671            self.change_form_count = len(initial)
    4772            self.required_forms = len(initial)
    48             self.total_forms = self.required_forms + self.num_extra
     73            self.total_forms = self.required_forms + self._meta.num_extra
    4974            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
    5075        else:
    5176            self.change_form_count = 0
    5277            self.required_forms = 0
    53             self.total_forms = self.num_extra
     78            self.total_forms = self._meta.num_extra
    5479            self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix)
    5580
    5681    def _get_add_forms(self):
    5782        """Return a list of all the add forms in this ``FormSet``."""
    58         FormClass = self.form_class
    5983        if not hasattr(self, '_add_forms'):
    6084            add_forms = []
    6185            for i in range(self.change_form_count, self.total_forms):
     
    6488                    kwargs['data'] = self.data
    6589                if self.files:
    6690                    kwargs['files'] = self.files
    67                 add_form = FormClass(**kwargs)
     91                add_form = self.get_form_class(i)(**kwargs)
    6892                self.add_fields(add_form, i)
    6993                add_forms.append(add_form)
    7094            self._add_forms = add_forms
     
    7397
    7498    def _get_change_forms(self):
    7599        """Return a list of all the change forms in this ``FormSet``."""
    76         FormClass = self.form_class
    77100        if not hasattr(self, '_change_forms'):
    78101            change_forms = []
    79102            for i in range(0, self.change_form_count):
     
    84107                    kwargs['files'] = self.files
    85108                if self.initial:
    86109                    kwargs['initial'] = self.initial[i]
    87                 change_form = FormClass(**kwargs)
     110                change_form = self.get_form_class(i)(**kwargs)
    88111                self.add_fields(change_form, i)
    89112                change_forms.append(change_form)
    90             self._change_forms= change_forms
     113            self._change_forms = change_forms
    91114        return self._change_forms
    92115    change_forms = property(_get_change_forms)
    93116
     
    117140        # Process change forms
    118141        for form in self.change_forms:
    119142            if form.is_valid():
    120                 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     143                if self._meta.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    121144                    self.deleted_data.append(form.cleaned_data)
    122145                else:
    123146                    self.cleaned_data.append(form.cleaned_data)
     
    144167        add_errors.reverse()
    145168        errors.extend(add_errors)
    146169        # Sort cleaned_data if the formset is orderable.
    147         if self.orderable:
     170        if self._meta.orderable:
    148171            self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME])
    149172        # Give self.clean() a chance to do validation
    150173        try:
     
    168191        """
    169192        return self.cleaned_data
    170193
     194    def get_form_class(self, index):
     195        """
     196        A hook to change a form class object.
     197        """
     198        FormClass = self._meta.form
     199        FormClass.base_fields = self.base_fields
     200        return FormClass
     201
    171202    def add_fields(self, form, index):
    172203        """A hook for adding extra fields on to each form instance."""
    173         if self.orderable:
     204        if self._meta.orderable:
    174205            form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1)
    175         if self.deletable:
     206        if self._meta.deletable:
    176207            form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
    177208
    178209    def add_prefix(self, index):
     
    193224            return Media()
    194225    media = property(_get_media)
    195226   
    196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False):
     227class FormSet(BaseFormSet):
     228    __metaclass__ = BaseFormSetMetaclass
     229   
     230def formset_for_form(form, formset=FormSet, num_extra=1, orderable=False,
     231                     deletable=False):
     232
    197233    """Return a FormSet for the given form class."""
    198     attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable}
    199     return type(form.__name__ + 'FormSet', (formset,), attrs)
     234    warn("formset_for_form is deprecated, use FormSet instead.",
     235         PendingDeprecationWarning,
     236         stacklevel=3)
     237    return BaseFormSetMetaclass(
     238        form.__name__ + "FormSet", (formset,), form.base_fields,
     239        form=form, num_extra=num_extra, orderable=orderable,
     240        deletable=deletable)
    200241
    201242def all_valid(formsets):
    202243    """Returns true if every formset in formsets is valid."""
  • django/newforms/models.py

     
    1313from util import ValidationError, ErrorList
    1414from forms import BaseForm
    1515from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
    16 from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME
     16from formsets import FormSetOptions, BaseFormSet, formset_for_form, DELETION_FIELD_NAME
     17from options import ModelFormOptions, ModelFormSetOptions, InlineFormSetOptions
    1718from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
    1819
    1920__all__ = (
    2021    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    2122    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
     23    'ModelFormSet', 'InlineFormset', 'modelform_for_model',
    2224    'formset_for_model', 'inline_formset',
    2325    'ModelChoiceField', 'ModelMultipleChoiceField',
    2426)
     
    207209            field_list.append((f.name, formfield))
    208210    return SortedDict(field_list)
    209211
    210 class ModelFormOptions(object):
    211     def __init__(self, options=None):
    212         self.model = getattr(options, 'model', None)
    213         self.fields = getattr(options, 'fields', None)
    214         self.exclude = getattr(options, 'exclude', None)
    215 
    216212class ModelFormMetaclass(type):
     213   
     214    opts_class = ModelFormOptions
     215   
    217216    def __new__(cls, name, bases, attrs,
    218                 formfield_callback=lambda f: f.formfield()):
     217                formfield_callback=lambda f: f.formfield(), **options):
    219218        fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
    220219        fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
    221220
     
    227226                fields = base.base_fields.items() + fields
    228227        declared_fields = SortedDict(fields)
    229228
    230         opts = ModelFormOptions(attrs.get('Meta', None))
     229        opts = cls.opts_class(options and options or attrs.get('Meta', None))
    231230        attrs['_meta'] = opts
    232231
    233232        # Don't allow more than one Meta model defenition in bases. The fields
     
    293292class ModelForm(BaseModelForm):
    294293    __metaclass__ = ModelFormMetaclass
    295294
     295# this should really be named form_for_model.
     296def modelform_for_model(model, form=ModelForm,
     297                        formfield_callback=lambda f: f.formfield(), **options):
     298    opts = model._meta
     299    options.update({"model": model})
     300    return ModelFormMetaclass(opts.object_name + "ModelForm", (form,),
     301                              {}, formfield_callback, **options)
    296302
    297303# Fields #####################################################################
    298304
     
    407413
    408414# Model-FormSet integration ###################################################
    409415
    410 def initial_data(instance, fields=None):
    411     """
    412     Return a dictionary from data in ``instance`` that is suitable for
    413     use as a ``Form`` constructor's ``initial`` argument.
     416class ModelFormSetMetaclass(ModelFormMetaclass):
     417    opts_class = ModelFormSetOptions
    414418
    415     Provide ``fields`` to specify the names of specific fields to return.
    416     All field values in the instance will be returned if ``fields`` is not
    417     provided.
    418     """
    419     # avoid a circular import
    420     from django.db.models.fields.related import ManyToManyField
    421     opts = instance._meta
    422     initial = {}
    423     for f in opts.fields + opts.many_to_many:
    424         if not f.editable:
    425             continue
    426         if fields and not f.name in fields:
    427             continue
    428         if isinstance(f, ManyToManyField):
    429             # MultipleChoiceWidget needs a list of ints, not object instances.
    430             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)]
    431         else:
    432             initial[f.name] = f.value_from_object(instance)
    433     return initial
    434 
    435419class BaseModelFormSet(BaseFormSet):
    436420    """
    437421    A ``FormSet`` for editing a queryset and/or adding new objects to it.
     
    439423    model = None
    440424    queryset = None
    441425
    442     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None):
     426    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None):
    443427        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
    444         self.queryset = qs
    445         kwargs['initial'] = [initial_data(obj) for obj in qs]
     428        opts = self._meta
     429        self.queryset = self.get_queryset(**kwargs)
     430        initial_data = []
     431        for obj in self.queryset:
     432            initial_data.append(model_to_dict(obj, opts.fields, opts.exclude))
     433        kwargs['initial'] = initial_data
     434
    446435        super(BaseModelFormSet, self).__init__(**kwargs)
    447436
     437    def get_queryset(self, **kwargs):
     438        """
     439        Hook to returning a queryset for this model.
     440        """
     441        return self._meta.model._default_manager.all()
     442
    448443    def save_new(self, form, commit=True):
    449444        """Saves and returns a new model instance for the given form."""
    450         return save_instance(form, self.model(), commit=commit)
     445        return save_instance(form, self._meta.model(), commit=commit)
    451446
    452447    def save_instance(self, form, instance, commit=True):
    453448        """Saves and returns an existing model instance for the given form."""
    454         return save_instance(form, instance, commit=commit)
     449        return save_instance(form, self._meta.model(), commit=commit)
    455450
    456451    def save(self, commit=True):
    457452        """Saves model instances for every form, adding and changing instances
     
    464459            return []
    465460        # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
    466461        existing_objects = {}
     462        opts = self._meta
    467463        for obj in self.queryset:
    468464            existing_objects[obj.pk] = obj
    469465        saved_instances = []
    470466        for form in self.change_forms:
    471             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
    472             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     467            obj = existing_objects[form.cleaned_data[opts.model._meta.pk.attname]]
     468            if opts.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    473469                obj.delete()
    474470            else:
    475471                saved_instances.append(self.save_instance(form, obj, commit=commit))
     
    483479            # If someone has marked an add form for deletion, don't save the
    484480            # object. At some point it would be nice if we didn't display
    485481            # the deletion widget for add forms.
    486             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
     482            opts = self._meta
     483            if opts.self.deletable and form.cleaned_data[DELETION_FIELD_NAME]:
    487484                continue
    488485            new_objects.append(self.save_new(form, commit=commit))
    489486        return new_objects
    490487
    491488    def add_fields(self, form, index):
    492489        """Add a hidden field for the object's primary key."""
    493         self._pk_field_name = self.model._meta.pk.attname
     490        self._pk_field_name = self._meta.model._meta.pk.attname
    494491        form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
    495492        super(BaseModelFormSet, self).add_fields(form, index)
    496493
    497 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(),
    498                       formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None):
     494class ModelFormSet(BaseModelFormSet):
     495    __metaclass__ = ModelFormSetMetaclass
     496
     497def formset_for_model(model, formset=BaseModelFormSet,
     498                      formfield_callback=lambda f: f.formfield(), **options):
    499499    """
    500500    Returns a FormSet class for the given Django model class. This FormSet
    501501    will contain change forms for every instance of the given model as well
     
    504504    This is essentially the same as ``formset_for_queryset``, but automatically
    505505    uses the model's default manager to determine the queryset.
    506506    """
    507     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback)
    508     FormSet = formset_for_form(form, formset, extra, orderable, deletable)
    509     FormSet.model = model
    510     return FormSet
     507    opts = model._meta
     508    options.update({"model": model})
     509    return ModelFormSetMetaclass(opts.object_name + "ModelFormSet", (formset,),
     510                                 {}, **options)
    511511
    512 class InlineFormset(BaseModelFormSet):
     512class InlineFormSetMetaclass(ModelFormSetMetaclass):
     513    opts_class = InlineFormSetOptions
     514
     515    def __new__(cls, name, bases, attrs,
     516                formfield_callback=lambda f: f.formfield(), **options):
     517        formset = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases, attrs,
     518            formfield_callback, **options)
     519        # If this isn't a subclass of InlineFormset, don't do anything special.
     520        try:
     521            if not filter(lambda b: issubclass(b, InlineFormset), bases):
     522                return formset
     523        except NameError:
     524            # 'InlineFormset' isn't defined yet, meaning we're looking at
     525            # Django's own InlineFormset class, defined below.
     526            return formset
     527        opts = formset._meta
     528        # resolve the foreign key
     529        fk = cls.resolve_foreign_key(opts.parent_model, opts.model, opts.fk_name)
     530        # remove the fk from base_fields to keep it transparent to the form.
     531        try:
     532            del formset.base_fields[fk.name]
     533        except KeyError:
     534            pass
     535        formset.fk = fk
     536        return formset
     537
     538    def _resolve_foreign_key(cls, parent_model, model, fk_name=None):
     539        """
     540        Finds and returns the ForeignKey from model to parent if there is one.
     541        If fk_name is provided, assume it is the name of the ForeignKey field.
     542        """
     543        # avoid a circular import
     544        from django.db.models import ForeignKey
     545        opts = model._meta
     546        if fk_name:
     547            fks_to_parent = [f for f in opts.fields if f.name == fk_name]
     548            if len(fks_to_parent) == 1:
     549                fk = fks_to_parent[0]
     550                if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
     551                    raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
     552            elif len(fks_to_parent) == 0:
     553                raise Exception("%s has no field named '%s'" % (model, fk_name))
     554        else:
     555            # Try to discover what the ForeignKey from model to parent_model is
     556            fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
     557            if len(fks_to_parent) == 1:
     558                fk = fks_to_parent[0]
     559            elif len(fks_to_parent) == 0:
     560                raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
     561            else:
     562                raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
     563        return fk
     564    resolve_foreign_key = classmethod(_resolve_foreign_key)
     565
     566class BaseInlineFormSet(BaseModelFormSet):
    513567    """A formset for child objects related to a parent."""
    514     def __init__(self, instance, data=None, files=None):
     568    def __init__(self, *args, **kwargs):
    515569        from django.db.models.fields.related import RelatedObject
    516         self.instance = instance
     570        opts = self._meta
     571        self.instance = kwargs.pop("instance", None)
    517572        # is there a better way to get the object descriptor?
    518         self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
    519         qs = self.get_queryset()
    520         super(InlineFormset, self).__init__(qs, data, files, prefix=self.rel_name)
     573        rel_name = RelatedObject(self.fk.rel.to, opts.model, self.fk).get_accessor_name()
     574        kwargs["prefix"] = rel_name
     575        super(BaseInlineFormSet, self).__init__(*args, **kwargs)
    521576
    522     def get_queryset(self):
     577    def get_queryset(self, **kwargs):
    523578        """
    524579        Returns this FormSet's queryset, but restricted to children of
    525580        self.instance
    526581        """
    527         kwargs = {self.fk.name: self.instance}
    528         return self.model._default_manager.filter(**kwargs)
     582        queryset = super(BaseInlineFormSet, self).get_queryset(**kwargs)
     583        return queryset.filter(**{self.fk.name: self.instance})
    529584
    530585    def save_new(self, form, commit=True):
    531586        kwargs = {self.fk.get_attname(): self.instance.pk}
    532         new_obj = self.model(**kwargs)
     587        new_obj = self._meta.model(**kwargs)
    533588        return save_instance(form, new_obj, commit=commit)
     589       
     590class InlineFormset(BaseInlineFormSet):
     591    __metaclass__ = InlineFormSetMetaclass
    534592
    535 def get_foreign_key(parent_model, model, fk_name=None):
     593def inline_formset(parent_model, model, formset=InlineFormset,
     594                   formfield_callback=lambda f: f.formfield(), **options):
    536595    """
    537     Finds and returns the ForeignKey from model to parent if there is one.
    538     If fk_name is provided, assume it is the name of the ForeignKey field.
    539     """
    540     # avoid circular import
    541     from django.db.models import ForeignKey
    542     opts = model._meta
    543     if fk_name:
    544         fks_to_parent = [f for f in opts.fields if f.name == fk_name]
    545         if len(fks_to_parent) == 1:
    546             fk = fks_to_parent[0]
    547             if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
    548                 raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
    549         elif len(fks_to_parent) == 0:
    550             raise Exception("%s has no field named '%s'" % (model, fk_name))
    551     else:
    552         # Try to discover what the ForeignKey from model to parent_model is
    553         fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
    554         if len(fks_to_parent) == 1:
    555             fk = fks_to_parent[0]
    556         elif len(fks_to_parent) == 0:
    557             raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
    558         else:
    559             raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
    560     return fk
    561 
    562 def inline_formset(parent_model, model, fk_name=None, fields=None, extra=3, orderable=False, deletable=True, formfield_callback=lambda f: f.formfield()):
    563     """
    564596    Returns an ``InlineFormset`` for the given kwargs.
    565597
    566598    You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
    567599    to ``parent_model``.
    568600    """
    569     fk = get_foreign_key(parent_model, model, fk_name=fk_name)
    570     # let the formset handle object deletion by default
    571     FormSet = formset_for_model(model, formset=InlineFormset, fields=fields,
    572                                 formfield_callback=formfield_callback,
    573                                 extra=extra, orderable=orderable,
    574                                 deletable=deletable)
    575     # HACK: remove the ForeignKey to the parent from every form
    576     # This should be done a line above before we pass 'fields' to formset_for_model
    577     # an 'omit' argument would be very handy here
    578     try:
    579         del FormSet.form_class.base_fields[fk.name]
    580     except KeyError:
    581         pass
    582     FormSet.fk = fk
    583     return FormSet
     601    opts = model._meta
     602    options.update({"parent_model": parent_model, "model": model})
     603    return InlineFormSetMetaclass(opts.object_name + "InlineFormset", (formset,))
  • tests/modeltests/model_formsets/models.py

     
    1616
    1717__test__ = {'API_TESTS': """
    1818
    19 >>> from django.newforms.models import formset_for_model
     19>>> from django import newforms as forms
     20>>> from django.newforms.models import formset_for_model, ModelFormSet
    2021
    21 >>> qs = Author.objects.all()
    22 >>> AuthorFormSet = formset_for_model(Author, extra=3)
     22A bare bones verion.
    2323
    24 >>> formset = AuthorFormSet(qs)
     24>>> class AuthorFormSet(ModelFormSet):
     25...     class Meta:
     26...         model = Author
     27>>> AuthorFormSet.base_fields.keys()
     28['name']
     29
     30Extra fields.
     31
     32>>> class AuthorFormSet(ModelFormSet):
     33...     published = forms.BooleanField()
     34...
     35...     class Meta:
     36...         model = Author
     37>>> AuthorFormSet.base_fields.keys()
     38['name', 'published']
     39
     40Lets create a formset that is bound to a model.
     41
     42>>> class AuthorFormSet(ModelFormSet):
     43...     class Meta:
     44...         model = Author
     45...         num_extra = 3
     46
     47>>> formset = AuthorFormSet()
    2548>>> for form in formset.forms:
    2649...     print form.as_p()
    2750<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
     
    3558...     'form-2-name': '',
    3659... }
    3760
    38 >>> formset = AuthorFormSet(qs, data=data)
     61>>> formset = AuthorFormSet(data)
    3962>>> formset.is_valid()
    4063True
    4164
     
    4972
    5073
    5174Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing
    52 authors with an extra form to add him. This time we'll use formset_for_queryset.
    53 We *could* use formset_for_queryset to restrict the Author objects we edit,
    54 but in that case we'll use it to display them in alphabetical order by name.
     75authors with an extra form to add him. When subclassing ModelFormSet you can
     76override the get_queryset method to return any queryset we like, but in this
     77case we'll use it to display it in alphabetical order by name.
    5578
    56 >>> qs = Author.objects.order_by('name')
    57 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=False)
     79>>> class AuthorFormSet(ModelFormSet):
     80...     class Meta:
     81...         model = Author
     82...         num_extra = 1
     83...         deletable = False
     84...
     85...     def get_queryset(self, **kwargs):
     86...         qs = super(AuthorFormSet, self).get_queryset(**kwargs)
     87...         return qs.order_by('name')
    5888
    59 >>> formset = AuthorFormSet(qs)
     89>>> formset = AuthorFormSet()
    6090>>> for form in formset.forms:
    6191...     print form.as_p()
    6292<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p>
     
    73103...     'form-2-name': 'Paul Verlaine',
    74104... }
    75105
    76 >>> formset = AuthorFormSet(qs, data=data)
     106>>> formset = AuthorFormSet(data)
    77107>>> formset.is_valid()
    78108True
    79109
     
    90120This probably shouldn't happen, but it will. If an add form was marked for
    91121deltetion, make sure we don't save that form.
    92122
    93 >>> qs = Author.objects.order_by('name')
    94 >>> AuthorFormSet = formset_for_model(Author, extra=1, deletable=True)
     123>>> class AuthorFormSet(ModelFormSet):
     124...     class Meta:
     125...         model = Author
     126...         num_extra = 1
     127...         deletable = True
     128...
     129...     def get_queryset(self, **kwargs):
     130...         qs = super(AuthorFormSet, self).get_queryset(**kwargs)
     131...         return qs.order_by('name')
    95132
    96 >>> formset = AuthorFormSet(qs)
     133>>> formset = AuthorFormSet()
     134
    97135>>> for form in formset.forms:
    98136...     print form.as_p()
    99137<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p>
     
    117155...     'form-3-DELETE': 'on',
    118156... }
    119157
    120 >>> formset = AuthorFormSet(qs, data=data)
     158>>> formset = AuthorFormSet(data)
    121159>>> formset.is_valid()
    122160True
    123161
     
    134172We can also create a formset that is tied to a parent model. This is how the
    135173admin system's edit inline functionality works.
    136174
    137 >>> from django.newforms.models import inline_formset
     175>>> from django.newforms.models import inline_formset, InlineFormset
    138176
    139 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=3)
     177>>> class AuthorBooksFormSet(InlineFormset):
     178...     class Meta:
     179...         parent_model = Author
     180...         model = Book
     181...         num_extra = 3
     182...         deletable = False
     183
    140184>>> author = Author.objects.get(name='Charles Baudelaire')
    141185
    142 >>> formset = AuthorBooksFormSet(author)
     186>>> formset = AuthorBooksFormSet(instance=author)
    143187>>> for form in formset.forms:
    144188...     print form.as_p()
    145189<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
     
    153197...     'book_set-2-title': '',
    154198... }
    155199
    156 >>> formset = AuthorBooksFormSet(author, data=data)
     200>>> formset = AuthorBooksFormSet(data, instance=author)
    157201>>> formset.is_valid()
    158202True
    159203
     
    169213one. This time though, an edit form will be available for every existing
    170214book.
    171215
    172 >>> AuthorBooksFormSet = inline_formset(Author, Book, deletable=False, extra=2)
     216>>> class AuthorBooksFormSet(InlineFormset):
     217...     class Meta:
     218...         parent_model = Author
     219...         model = Book
     220...         num_extra = 2
     221...         deletable = False
     222
     223
    173224>>> author = Author.objects.get(name='Charles Baudelaire')
    174225
    175 >>> formset = AuthorBooksFormSet(author)
     226>>> formset = AuthorBooksFormSet(instance=author)
    176227>>> for form in formset.forms:
    177228...     print form.as_p()
    178229<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
     
    187238...     'book_set-2-title': '',
    188239... }
    189240
    190 >>> formset = AuthorBooksFormSet(author, data=data)
     241>>> formset = AuthorBooksFormSet(data, instance=author)
    191242>>> formset.is_valid()
    192243True
    193244
  • tests/regressiontests/forms/formsets.py

     
    22formset_tests = """
    33# Basic FormSet creation and usage ############################################
    44
    5 FormSet allows us to use multiple instance of the same form on 1 page. For now,
    6 the best way to create a FormSet is by using the formset_for_form function.
     5FormSet allows us to use multiple instance of the same form on 1 page. Create
     6the formset as you would a regular form by defining the fields declaratively.
    77
    8 >>> from django.newforms import Form, CharField, IntegerField, ValidationError
    9 >>> from django.newforms.formsets import formset_for_form, BaseFormSet
     8>>> from django.newforms import BooleanField
     9>>> from django.newforms.formsets import formset_for_form, BaseFormSet, FormSet
    1010
    11 >>> class Choice(Form):
     11>>> class ChoiceFormSet(FormSet):
    1212...     choice = CharField()
    1313...     votes = IntegerField()
    1414
    15 >>> ChoiceFormSet = formset_for_form(Choice)
    16 
    17 
    1815A FormSet constructor takes the same arguments as Form. Let's create a FormSet
    1916for adding data. By default, it displays 1 blank form. It can display more,
    2017but we'll look at how to do so later.
     
    145142>>> formset.errors
    146143[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}]
    147144
     145# Subclassing a FormSet class #################################################
    148146
     147We can subclass a FormSet to add addition fields to an already exisiting
     148FormSet.
     149
     150>>> class SecondChoiceFormSet(ChoiceFormSet):
     151...     is_public = BooleanField()
     152
     153>>> formset = SecondChoiceFormSet(auto_id=False, prefix="choices")
     154>>> for form in formset.forms:
     155...     print form.as_ul()
     156<li>Choice: <input type="text" name="choices-0-choice" /></li>
     157<li>Votes: <input type="text" name="choices-0-votes" /></li>
     158<li>Is public: <input type="checkbox" name="choices-0-is_public" /></li>
     159
    149160# Displaying more than 1 blank form ###########################################
    150161
    151 We can also display more than 1 empty form at a time. To do so, pass a
    152 num_extra argument to formset_for_form.
     162We can also display more than 1 empty form at a time. To do so, create an inner
     163Meta class with an attribute num_extra.
    153164
    154 >>> ChoiceFormSet = formset_for_form(Choice, num_extra=3)
     165>>> class NumExtraChoiceFormSet(ChoiceFormSet):
     166...     class Meta:
     167...         num_extra = 3
    155168
    156 >>> formset = ChoiceFormSet(auto_id=False, prefix='choices')
     169>>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices')
    157170>>> for form in formset.forms:
    158171...    print form.as_ul()
    159172<li>Choice: <input type="text" name="choices-0-choice" /></li>
     
    177190...     'choices-2-votes': '',
    178191... }
    179192
    180 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     193>>> formset = NumExtraChoiceFormSet(auto_id=False, prefix='choices')
    181194>>> formset.is_valid()
    182195True
    183196>>> formset.cleaned_data
     
    196209...     'choices-2-votes': '',
    197210... }
    198211
    199 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     212>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    200213>>> formset.is_valid()
    201214True
    202215>>> formset.cleaned_data
     
    215228...     'choices-2-votes': '',
    216229... }
    217230
    218 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     231>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    219232>>> formset.is_valid()
    220233False
    221234>>> formset.errors
     
    226239data.
    227240
    228241>>> initial = [{'choice': u'Calexico', 'votes': 100}]
    229 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     242>>> formset = NumExtraChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    230243>>> for form in formset.forms:
    231244...    print form.as_ul()
    232245<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
     
    254267...     'choices-3-votes': '',
    255268... }
    256269
    257 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     270>>> formset = NumExtraChoiceFormSet(data, auto_id=False, prefix='choices')
    258271>>> formset.is_valid()
    259272False
    260273>>> formset.errors
     
    263276
    264277# FormSets with deletion ######################################################
    265278
    266 We can easily add deletion ability to a FormSet with an agrument to
    267 formset_for_form. This will add a boolean field to each form instance. When
    268 that boolean field is True, the cleaned data will be in formset.deleted_data
     279We can easily add deletion ability to a FormSet by setting deletable to True
     280in the inner Meta class. This will add a boolean field to each form instance.
     281When that boolean field is True, the cleaned data will be in formset.deleted_data
    269282rather than formset.cleaned_data
    270283
    271 >>> ChoiceFormSet = formset_for_form(Choice, deletable=True)
     284>>> class DeletableChoiceFormSet(ChoiceFormSet):
     285...     class Meta:
     286...         deletable = True
    272287
    273288>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
    274 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     289>>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices')
    275290>>> for form in formset.forms:
    276291...    print form.as_ul()
    277292<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
     
    300315...     'choices-2-DELETE': '',
    301316... }
    302317
    303 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     318>>> formset = DeletableChoiceFormSet(data, auto_id=False, prefix='choices')
    304319>>> formset.is_valid()
    305320True
    306321>>> formset.cleaned_data
     
    310325
    311326# FormSets with ordering ######################################################
    312327
    313 We can also add ordering ability to a FormSet with an agrument to
    314 formset_for_form. This will add a integer field to each form instance. When
     328We can also add ordering ability to a FormSet by setting orderable to True in
     329the inner Meta class. This will add a integer field to each form instance. When
    315330form validation succeeds, formset.cleaned_data will have the data in the correct
    316331order specified by the ordering fields. If a number is duplicated in the set
    317332of ordering fields, for instance form 0 and form 3 are both marked as 1, then
    318333the form index used as a secondary ordering criteria. In order to put
    319334something at the front of the list, you'd need to set it's order to 0.
    320335
    321 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True)
     336>>> class OrderableChoiceFormSet(ChoiceFormSet):
     337...     class Meta:
     338...         orderable = True
    322339
    323340>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}]
    324 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     341>>> formset = OrderableChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    325342>>> for form in formset.forms:
    326343...    print form.as_ul()
    327344<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
     
    347364...     'choices-2-ORDER': '0',
    348365... }
    349366
    350 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     367>>> formset = OrderableChoiceFormSet(data, auto_id=False, prefix='choices')
    351368>>> formset.is_valid()
    352369True
    353370>>> for cleaned_data in formset.cleaned_data:
     
    359376# FormSets with ordering + deletion ###########################################
    360377
    361378Let's try throwing ordering and deletion into the same form.
     379TODO: Perhaps handle Meta class inheritance so you can subclass
     380OrderableChoiceFormSet and DeletableChoiceFormSet?
    362381
    363 >>> ChoiceFormSet = formset_for_form(Choice, orderable=True, deletable=True)
     382>>> class MixedChoiceFormSet(ChoiceFormSet):
     383...     class Meta:
     384...         orderable = True
     385...         deletable = True
    364386
     387>>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    365388>>> initial = [
    366389...     {'choice': u'Calexico', 'votes': 100},
    367390...     {'choice': u'Fergie', 'votes': 900},
    368391...     {'choice': u'The Decemberists', 'votes': 500},
    369392... ]
    370 >>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
     393>>> formset = MixedChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
    371394>>> for form in formset.forms:
    372395...    print form.as_ul()
    373396<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
     
    409432...     'choices-3-DELETE': '',
    410433... }
    411434
    412 >>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
     435>>> formset = MixedChoiceFormSet(data, auto_id=False, prefix='choices')
    413436>>> formset.is_valid()
    414437True
    415438>>> for cleaned_data in formset.cleaned_data:
     
    428451Let's define a FormSet that takes a list of favorite drinks, but raises am
    429452error if there are any duplicates.
    430453
    431 >>> class FavoriteDrinkForm(Form):
     454>>> class FavoriteDrinkForm(FormSet):
    432455...     name = CharField()
    433456...
    434 
    435 >>> class FavoriteDrinksFormSet(BaseFormSet):
    436 ...     form_class = FavoriteDrinkForm
    437 ...     num_extra = 2
    438 ...     orderable = False
    439 ...     deletable = False
     457...     class Meta:
     458...         num_extra = 2
     459...         orderable = False
     460...         deletable = False
    440461...
    441462...     def clean(self):
    442463...         seen_drinks = []
  • tests/regressiontests/inline_formsets/models.py

     
    2121Child has two ForeignKeys to Parent, so if we don't specify which one to use
    2222for the inline formset, we should get an exception.
    2323
    24 >>> ifs = inline_formset(Parent, Child)
     24>>> inline_formset(Parent, Child)()
    2525Traceback (most recent call last):
    2626    ...
    2727Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
     
    2929
    3030These two should both work without a problem.
    3131
    32 >>> ifs = inline_formset(Parent, Child, fk_name='mother')
    33 >>> ifs = inline_formset(Parent, Child, fk_name='father')
     32+>>> ifs = inline_formset(Parent, Child, fk_name='mother')()
     33+>>> ifs = inline_formset(Parent, Child, fk_name='father')()
    3434
    3535
    3636If we specify fk_name, but it isn't a ForeignKey from the child model to the
    3737parent model, we should get an exception.
    3838
    39 >>> ifs = inline_formset(Parent, Child, fk_name='school')
     39>>> inline_formset(Parent, Child, fk_name='school')()
    4040Traceback (most recent call last):
    4141    ...
    4242Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'>
     
    4545If the field specified in fk_name is not a ForeignKey, we should get an
    4646exception.
    4747
    48 >>> ifs = inline_formset(Parent, Child, fk_name='test')
     48>>> inline_formset(Parent, Child, fk_name='test')()
    4949Traceback (most recent call last):
    5050    ...
    5151Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test'
Back to Top