Django

Code

Changeset 7270

Show
Ignore:
Timestamp:
03/17/08 12:55:16 (4 months ago)
Author:
jkocherhans
Message:

newforms-admin: Cleaned up the implementation and APIs of all the formset classes. Backwards-incompatible.

Files:

Legend:

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

    r7195 r7270  
    22from django import newforms as forms 
    33from django.newforms.formsets import all_valid 
     4from django.newforms.models import _modelform_factory, _inlineformset_factory 
    45from django.contrib.contenttypes.models import ContentType 
    56from django.contrib.admin import widgets 
     
    341342        else: 
    342343            fields = None 
    343         return forms.form_for_model(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 
     344        return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 
    344345 
    345346    def form_change(self, request, obj): 
     
    351352        else: 
    352353            fields = None 
    353         return forms.form_for_instance(obj, fields=fields, formfield_callback=self.formfield_for_dbfield) 
     354        return _modelform_factory(self.model, fields=fields, formfield_callback=self.formfield_for_dbfield) 
    354355 
    355356    def save_add(self, request, model, form, formsets, post_url_continue): 
     
    497498            form = ModelForm(request.POST, request.FILES) 
    498499            for FormSet in self.formsets_add(request): 
    499                 inline_formset = FormSet(obj, data=request.POST, files=request.FILES
     500                inline_formset = FormSet(data=request.POST, files=request.FILES, instance=obj
    500501                inline_formsets.append(inline_formset) 
    501502            if all_valid(inline_formsets) and form.is_valid(): 
     
    504505            form = ModelForm(initial=request.GET) 
    505506            for FormSet in self.formsets_add(request): 
    506                 inline_formset = FormSet(obj) 
     507                inline_formset = FormSet(instance=obj) 
    507508                inline_formsets.append(inline_formset) 
    508509 
     
    554555        inline_formsets = [] 
    555556        if request.method == 'POST': 
    556             form = ModelForm(request.POST, request.FILES
     557            form = ModelForm(request.POST, request.FILES, instance=obj
    557558            for FormSet in self.formsets_change(request, obj): 
    558                 inline_formset = FormSet(obj, request.POST, request.FILES
     559                inline_formset = FormSet(request.POST, request.FILES, instance=obj
    559560                inline_formsets.append(inline_formset) 
    560561 
     
    562563                return self.save_change(request, model, form, inline_formsets) 
    563564        else: 
    564             form = ModelForm(
     565            form = ModelForm(instance=obj
    565566            for FormSet in self.formsets_change(request, obj): 
    566                 inline_formset = FormSet(obj) 
     567                inline_formset = FormSet(instance=obj) 
    567568                inline_formsets.append(inline_formset) 
    568569 
     
    741742        else: 
    742743            fields = None 
    743         return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 
     744        return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 
    744745 
    745746    def formset_change(self, request, obj): 
     
    749750        else: 
    750751            fields = None 
    751         return forms.inline_formset(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 
     752        return _inlineformset_factory(self.parent_model, self.model, fk_name=self.fk_name, fields=fields, formfield_callback=self.formfield_for_dbfield, extra=self.extra) 
    752753 
    753754    def fieldsets_add(self, request): 
    754755        if self.declared_fieldsets: 
    755756            return self.declared_fieldsets 
    756         form = self.formset_add(request).form_class 
     757        form = self.formset_add(request).form 
    757758        return [(None, {'fields': form.base_fields.keys()})] 
    758759 
     
    760761        if self.declared_fieldsets: 
    761762            return self.declared_fieldsets 
    762         form = self.formset_change(request, obj).form_class 
     763        form = self.formset_change(request, obj).form 
    763764        return [(None, {'fields': form.base_fields.keys()})] 
    764765 
     
    779780 
    780781    def __iter__(self): 
    781         for form, original in zip(self.formset.change_forms, self.formset.get_queryset()): 
     782        for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): 
    782783            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original) 
    783         for form in self.formset.add_forms: 
     784        for form in self.formset.extra_forms: 
    784785            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None) 
    785786 
    786787    def fields(self): 
    787788        for field_name in flatten_fieldsets(self.fieldsets): 
    788             yield self.formset.form_class.base_fields[field_name] 
     789            yield self.formset.form.base_fields[field_name] 
    789790 
    790791class InlineAdminForm(AdminForm): 
  • django/branches/newforms-admin/django/newforms/formsets.py

    r6419 r7270  
    11from forms import Form 
     2from django.utils.encoding import StrAndUnicode 
    23from fields import IntegerField, BooleanField 
    3 from widgets import HiddenInput, Media 
     4from widgets import HiddenInput, TextInput 
    45from util import ErrorList, ValidationError 
    56 
    6 __all__ = ('BaseFormSet', 'formset_for_form', 'all_valid') 
     7__all__ = ('BaseFormSet', 'all_valid') 
    78 
    89# special field names 
    9 FORM_COUNT_FIELD_NAME = 'COUNT' 
     10TOTAL_FORM_COUNT = 'TOTAL_FORMS' 
     11INITIAL_FORM_COUNT = 'INITIAL_FORMS' 
    1012ORDERING_FIELD_NAME = 'ORDER' 
    1113DELETION_FIELD_NAME = 'DELETE' 
     
    1820    """ 
    1921    def __init__(self, *args, **kwargs): 
    20         self.base_fields[FORM_COUNT_FIELD_NAME] = IntegerField(widget=HiddenInput) 
     22        self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 
     23        self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 
    2124        super(ManagementForm, self).__init__(*args, **kwargs) 
    2225 
    23 class BaseFormSet(object): 
    24     """A collection of instances of the same Form class.""" 
    25  
    26     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,  
     26class BaseFormSet(StrAndUnicode): 
     27    """ 
     28    A collection of instances of the same Form class. 
     29    """ 
     30    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
    2731            initial=None, error_class=ErrorList): 
    2832        self.is_bound = data is not None or files is not None 
     
    3337        self.initial = initial 
    3438        self.error_class = error_class 
     39        self._errors = None 
     40        self._non_form_errors = None 
    3541        # initialization is different depending on whether we recieved data, initial, or nothing 
    3642        if data or files: 
    3743            self.management_form = ManagementForm(data, files, auto_id=self.auto_id, prefix=self.prefix) 
    3844            if self.management_form.is_valid(): 
    39                 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 
     45                self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT] 
     46                self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT] 
    4247            else: 
    43                 # not sure that ValidationError is the best thing to raise here 
    4448                raise ValidationError('ManagementForm data is missing or has been tampered with') 
    4549        elif initial: 
    46             self.change_form_count = len(initial) 
    47             self.required_forms = len(initial) 
    48             self.total_forms = self.required_forms + self.num_extra 
    49             self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 
     50            self._initial_form_count = len(initial) 
     51            self._total_form_count = self._initial_form_count + self.extra 
    5052        else: 
    51             self.change_form_count = 0 
    52             self.required_forms = 0 
    53             self.total_forms = self.num_extra 
    54             self.management_form = ManagementForm(initial={FORM_COUNT_FIELD_NAME: self.total_forms}, auto_id=self.auto_id, prefix=self.prefix) 
    55  
    56     def _get_add_forms(self): 
    57         """Return a list of all the add forms in this ``FormSet``.""" 
    58         FormClass = self.form_class 
    59         if not hasattr(self, '_add_forms'): 
    60             add_forms = [] 
    61             for i in range(self.change_form_count, self.total_forms): 
    62                 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 
    63                 if self.data: 
    64                     kwargs['data'] = self.data 
    65                 if self.files: 
    66                     kwargs['files'] = self.files 
    67                 add_form = FormClass(**kwargs) 
    68                 self.add_fields(add_form, i) 
    69                 add_forms.append(add_form) 
    70             self._add_forms = add_forms 
    71         return self._add_forms 
    72     add_forms = property(_get_add_forms) 
    73  
    74     def _get_change_forms(self): 
    75         """Return a list of all the change forms in this ``FormSet``.""" 
    76         FormClass = self.form_class 
    77         if not hasattr(self, '_change_forms'): 
    78             change_forms = [] 
    79             for i in range(0, self.change_form_count): 
    80                 kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 
    81                 if self.data: 
    82                     kwargs['data'] = self.data 
    83                 if self.files: 
    84                     kwargs['files'] = self.files 
    85                 if self.initial: 
    86                     kwargs['initial'] = self.initial[i] 
    87                 change_form = FormClass(**kwargs) 
    88                 self.add_fields(change_form, i) 
    89                 change_forms.append(change_form) 
    90             self._change_forms= change_forms 
    91         return self._change_forms 
    92     change_forms = property(_get_change_forms) 
    93  
    94     def _forms(self): 
    95         return self.change_forms + self.add_forms 
    96     forms = property(_forms) 
     53            self._initial_form_count = 0 
     54            self._total_form_count = self.extra 
     55        initial = {TOTAL_FORM_COUNT: self._total_form_count, INITIAL_FORM_COUNT: self._initial_form_count} 
     56        self.management_form = ManagementForm(initial=initial, auto_id=auto_id, prefix=prefix) 
     57 
     58        # instantiate all the forms and put them in self.forms 
     59        self.forms = [] 
     60        for i in range(self._total_form_count): 
     61            self.forms.append(self._construct_form(i)) 
     62 
     63    def __unicode__(self): 
     64        return self.as_table() 
     65 
     66    def _construct_form(self, i): 
     67        """ 
     68        Instantiates and returns the i-th form instance in a formset. 
     69        """ 
     70        kwargs = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)} 
     71        if self.data or self.files: 
     72            kwargs['data'] = self.data 
     73            kwargs['files'] = self.files 
     74        if self.initial: 
     75            try: 
     76                kwargs['initial'] = self.initial[i] 
     77            except IndexError: 
     78                pass 
     79        # Allow extra forms to be empty. 
     80        if i >= self._initial_form_count: 
     81            kwargs['empty_permitted'] = True 
     82        form = self.form(**kwargs) 
     83        self.add_fields(form, i) 
     84        return form 
     85 
     86    def _get_initial_forms(self): 
     87        """Return a list of all the intial forms in this formset.""" 
     88        return self.forms[:self._initial_form_count] 
     89    initial_forms = property(_get_initial_forms) 
     90 
     91    def _get_extra_forms(self): 
     92        """Return a list of all the extra forms in this formset.""" 
     93        return self.forms[self._initial_form_count:] 
     94    extra_forms = property(_get_extra_forms) 
     95 
     96    # Maybe this should just go away? 
     97    def _get_cleaned_data(self): 
     98        """ 
     99        Returns a list of form.cleaned_data dicts for every form in self.forms. 
     100        """ 
     101        if not self.is_valid(): 
     102            raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__) 
     103        return [form.cleaned_data for form in self.forms] 
     104    cleaned_data = property(_get_cleaned_data) 
     105 
     106    def _get_deleted_forms(self): 
     107        """ 
     108        Returns a list of forms that have been marked for deletion. Raises an  
     109        AttributeError is deletion is not allowed. 
     110        """ 
     111        if not self.is_valid() or not self.can_delete: 
     112            raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__) 
     113        # construct _deleted_form_indexes which is just a list of form indexes 
     114        # that have had their deletion widget set to True 
     115        if not hasattr(self, '_deleted_form_indexes'): 
     116            self._deleted_form_indexes = [] 
     117            for i in range(0, self._total_form_count): 
     118                form = self.forms[i] 
     119                # if this is an extra form and hasn't changed, don't consider it 
     120                if i >= self._initial_form_count and not form.has_changed(): 
     121                    continue 
     122                if form.cleaned_data[DELETION_FIELD_NAME]: 
     123                    self._deleted_form_indexes.append(i) 
     124        return [self.forms[i] for i in self._deleted_form_indexes] 
     125    deleted_forms = property(_get_deleted_forms) 
     126 
     127    def _get_ordered_forms(self): 
     128        """ 
     129        Returns a list of form in the order specified by the incoming data. 
     130        Raises an AttributeError is deletion is not allowed. 
     131        """ 
     132        if not self.is_valid() or not self.can_order: 
     133            raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__) 
     134        # Construct _ordering, which is a list of (form_index, order_field_value) 
     135        # tuples. After constructing this list, we'll sort it by order_field_value 
     136        # so we have a way to get to the form indexes in the order specified 
     137        # by the form data. 
     138        if not hasattr(self, '_ordering'): 
     139            self._ordering = [] 
     140            for i in range(0, self._total_form_count): 
     141                form = self.forms[i] 
     142                # if this is an extra form and hasn't changed, don't consider it 
     143                if i >= self._initial_form_count and not form.has_changed(): 
     144                    continue 
     145                # don't add data marked for deletion to self.ordered_data 
     146                if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: 
     147                    continue 
     148                # A sort function to order things numerically ascending, but 
     149                # None should be sorted below anything else. Allowing None as 
     150                # a comparison value makes it so we can leave ordering fields 
     151                # blamk. 
     152                def compare_ordering_values(x, y): 
     153                    if x[1] is None: 
     154                        return 1 
     155                    if y[1] is None: 
     156                        return -1 
     157                    return x[1] - y[1] 
     158                self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME])) 
     159            # After we're done populating self._ordering, sort it. 
     160            self._ordering.sort(compare_ordering_values) 
     161        # Return a list of form.cleaned_data dicts in the order spcified by 
     162        # the form data. 
     163        return [self.forms[i[0]] for i in self._ordering] 
     164    ordered_forms = property(_get_ordered_forms) 
    97165 
    98166    def non_form_errors(self): 
     
    102170        are none. 
    103171        """ 
    104         if hasattr(self, '_non_form_errors')
     172        if self._non_form_errors is not None
    105173            return self._non_form_errors 
    106174        return self.error_class() 
    107175 
     176    def _get_errors(self): 
     177        """ 
     178        Returns a list of form.errors for every form in self.forms. 
     179        """ 
     180        if self._errors is None: 
     181            self.full_clean() 
     182        return self._errors 
     183    errors = property(_get_errors) 
     184 
     185    def is_valid(self): 
     186        """ 
     187        Returns True if form.errors is empty for every form in self.forms. 
     188        """ 
     189        if not self.is_bound: 
     190            return False 
     191        # We loop over every form.errors here rather than short circuiting on the 
     192        # first failure to make sure validation gets triggered for every form. 
     193        forms_valid = True 
     194        for errors in self.errors: 
     195            if bool(errors): 
     196                forms_valid = False 
     197        return forms_valid and not bool(self.non_form_errors()) 
     198 
    108199    def full_clean(self): 
    109         """Cleans all of self.data and populates self.__errors and self.cleaned_data.""" 
    110         self._is_valid = True # Assume the formset is valid until proven otherwise. 
    111         errors = [] 
     200        """ 
     201        Cleans all of self.data and populates self._errors. 
     202        """ 
     203        self._errors = [] 
    112204        if not self.is_bound: # Stop further processing. 
    113             self.__errors = errors 
    114205            return 
    115         self.cleaned_data = [] 
    116         self.deleted_data = [] 
    117         # Process change forms 
    118         for form in self.change_forms: 
    119             if form.is_valid(): 
    120                 if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
    121                     self.deleted_data.append(form.cleaned_data) 
    122                 else: 
    123                     self.cleaned_data.append(form.cleaned_data) 
    124             else: 
    125                 self._is_valid = False 
    126             errors.append(form.errors) 
    127         # Process add forms in reverse so we can easily tell when the remaining 
    128         # ones should be required. 
    129         reamining_forms_required = False 
    130         add_errors = [] 
    131         for i in range(len(self.add_forms)-1, -1, -1): 
    132             form = self.add_forms[i] 
    133             # If an add form is empty, reset it so it won't have any errors 
    134             if form.is_empty([ORDERING_FIELD_NAME]) and not reamining_forms_required: 
    135                 form.reset() 
    136                 continue 
    137             else: 
    138                 reamining_forms_required = True 
    139                 if form.is_valid(): 
    140                     self.cleaned_data.append(form.cleaned_data) 
    141                 else: 
    142                     self._is_valid = False 
    143             add_errors.append(form.errors) 
    144         add_errors.reverse() 
    145         errors.extend(add_errors) 
    146         # Sort cleaned_data if the formset is orderable. 
    147         if self.orderable: 
    148             self.cleaned_data.sort(lambda x,y: x[ORDERING_FIELD_NAME] - y[ORDERING_FIELD_NAME]) 
    149         # Give self.clean() a chance to do validation 
     206        for i in range(0, self._total_form_count): 
     207            form = self.forms[i] 
     208            self._errors.append(form.errors) 
     209        # Give self.clean() a chance to do cross-form validation. 
    150210        try: 
    151             self.cleaned_data = self.clean() 
     211            self.clean() 
    152212        except ValidationError, e: 
    153213            self._non_form_errors = e.messages 
    154             self._is_valid = False 
    155         self.errors = errors 
    156         # If there were errors, be consistent with forms and remove the 
    157         # cleaned_data and deleted_data attributes. 
    158         if not self._is_valid: 
    159             delattr(self, 'cleaned_data') 
    160             delattr(self, 'deleted_data') 
    161214 
    162215    def clean(self): 
     
    167220        via formset.non_form_errors() 
    168221        """ 
    169         return self.cleaned_data 
     222        pass 
    170223 
    171224    def add_fields(self, form, index): 
    172225        """A hook for adding extra fields on to each form instance.""" 
    173         if self.orderable: 
    174             form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1) 
    175         if self.deletable: 
     226        if self.can_order: 
     227            # Only pre-fill the ordering field for initial forms. 
     228            if index < self._initial_form_count: 
     229                form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False) 
     230            else: 
     231                form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False) 
     232        if self.can_delete: 
    176233            form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False) 
    177234 
     
    179236        return '%s-%s' % (self.prefix, index) 
    180237 
    181     def is_valid(self): 
    182         if not self.is_bound: 
    183             return False 
    184         self.full_clean() 
    185         return self._is_valid 
     238    def is_multipart(self): 
     239        """ 
     240        Returns True if the formset needs to be multipart-encrypted, i.e. it 
     241        has FileInput. Otherwise, False. 
     242        """ 
     243        return self.forms[0].is_multipart() 
    186244 
    187245    def _get_media(self): 
    188         # All the forms on a FormSet are the same, so you only need to  
     246        # All the forms on a FormSet are the same, so you only need to 
    189247        # interrogate the first form for media. 
    190248        if self.forms: 
     
    193251            return Media() 
    194252    media = property(_get_media) 
    195      
    196 def formset_for_form(form, formset=BaseFormSet, num_extra=1, orderable=False, deletable=False): 
     253 
     254    def as_table(self): 
     255        "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>." 
     256        # XXX: there is no semantic division between forms here, there 
     257        # probably should be. It might make sense to render each form as a 
     258        # table row with each field as a td. 
     259        forms = u' '.join([form.as_table() for form in self.forms]) 
     260        return u'\n'.join([unicode(self.management_form), forms]) 
     261 
     262# XXX: This API *will* change. Use at your own risk. 
     263def _formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False): 
    197264    """Return a FormSet for the given form class.""" 
    198     attrs = {'form_class': form, 'num_extra': num_extra, 'orderable': orderable, 'deletable': deletable} 
     265    attrs = {'form': form, 'extra': extra, 'can_order': can_order, 'can_delete': can_delete} 
    199266    return type(form.__name__ + 'FormSet', (formset,), attrs) 
    200267 
  • django/branches/newforms-admin/django/newforms/forms.py

    r7233 r7270  
    7070    # class, not to the Form class. 
    7171    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
    72                  initial=None, error_class=ErrorList, label_suffix=':'): 
     72                 initial=None, error_class=ErrorList, label_suffix=':', 
     73                 empty_permitted=False): 
    7374        self.is_bound = data is not None or files is not None 
    7475        self.data = data or {} 
     
    7980        self.error_class = error_class 
    8081        self.label_suffix = label_suffix 
     82        self.empty_permitted = empty_permitted 
    8183        self._errors = None # Stores the errors after clean() has been called. 
    8284 
     
    190192        return self.errors.get(NON_FIELD_ERRORS, self.error_class()) 
    191193 
    192     def is_empty(self, exceptions=None): 
    193         """ 
    194         Returns True if this form has been bound and all fields that aren't 
    195         listed in exceptions are empty. 
    196         """ 
    197         # TODO: This could probably use some optimization 
    198         exceptions = exceptions or [] 
    199         for name, field in self.fields.items(): 
    200             if name in exceptions: 
    201                 continue 
    202             # value_from_datadict() gets the data from the data dictionaries. 
    203             # Each widget type knows how to retrieve its own data, because some 
    204             # widgets split data over several HTML fields. 
    205             value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) 
    206             if not field.widget.is_empty(value): 
    207                 return False 
    208         return True 
    209  
    210194    def full_clean(self): 
    211195        """ 
     
    217201            return 
    218202        self.cleaned_data = {} 
     203        # If the form is permitted to be empty, and none of the form data has 
     204        # changed from the initial data, short circuit any validation. 
     205        if self.empty_permitted and not self.has_changed(): 
     206            return 
    219207        for name, field in self.fields.items(): 
    220208            # value_from_datadict() gets the data from the data dictionaries. 
     
    252240        return self.cleaned_data 
    253241 
    254     def reset(self): 
    255         """Return this form to the state it was in before data was passed to it.""" 
    256         self.data = {} 
    257         self.is_bound = False 
    258         self.__errors = None 
     242    def has_changed(self): 
     243        """ 
     244        Returns True if data differs from initial. 
     245        """ 
     246        # XXX: For now we're asking the individual widgets whether or not the 
     247        # data has changed. It would probably be more efficient to hash the 
     248        # initial data, store it in a hidden field, and compare a hash of the 
     249        # submitted data, but we'd need a way to easily get the string value 
     250        # for a given field. Right now, that logic is embedded in the render 
     251        # method of each widget. 
     252        for name, field in self.fields.items(): 
     253            prefixed_name = self.add_prefix(name) 
     254            data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) 
     255            initial_value = self.initial.get(name, field.initial) 
     256            if field.widget._has_changed(initial_value, data_value): 
     257                #print field 
     258                return True 
     259        return False 
    259260 
    260261    def _get_media(self): 
  • django/branches/newforms-admin/django/newforms/models.py

    r7121 r7270  
    1414from forms import BaseForm, get_declared_fields 
    1515from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 
    16 from formsets import BaseFormSet, formset_for_form, DELETION_FIELD_NAME 
    1716from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 
     17from formsets import BaseFormSet, _formset_factory, DELETION_FIELD_NAME 
    1818 
    1919__all__ = ( 
    2020    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', 
    2121    'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', 
    22     'formset_for_model', 'inline_formset', 
    2322    'ModelChoiceField', 'ModelMultipleChoiceField', 
    2423) 
     
    246245    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, 
    247246                 initial=None, error_class=ErrorList, label_suffix=':', 
    248                  instance=None): 
     247                 empty_permitted=False, instance=None): 
    249248        opts = self._meta 
    250249        if instance is None: 
     
    258257        if initial is not None: 
    259258            object_data.update(initial) 
    260         BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix) 
     259        BaseForm.__init__(self, data, files, auto_id, prefix, object_data, 
     260                          error_class, label_suffix, empty_permitted) 
    261261 
    262262    def save(self, commit=True): 
     
    277277    __metaclass__ = ModelFormMetaclass 
    278278 
    279  
    280 # Fields ##################################################################### 
    281  
    282 class QuerySetIterator(object): 
    283     def __init__(self, queryset, empty_label, cache_choices): 
     279# XXX: This API *will* change. Use at your own risk. 
     280def _modelform_factory(model, form=BaseForm, fields=None, exclude=None, 
     281                       formfield_callback=lambda f: f.formfield()): 
     282    # HACK: we should be able to construct a ModelForm without creating 
     283    # and passing in a temporary inner class 
     284    class Meta: 
     285        pass 
     286    setattr(Meta, 'model', model) 
     287    setattr(Meta, 'fields', fields) 
     288    setattr(Meta, 'exclude', exclude) 
     289    class_name = model.__name__ + 'Form' 
     290    return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta}, 
     291                              formfield_callback=formfield_callback) 
     292 
     293 
     294# ModelFormSets ############################################################## 
     295 
     296class BaseModelFormSet(BaseFormSet): 
     297    """ 
     298    A ``FormSet`` for editing a queryset and/or adding new objects to it. 
     299    """ 
     300    model = None 
     301 
     302    def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, queryset=None): 
    284303        self.queryset = queryset 
    285         self.empty_label = empty_label 
    286         self.cache_choices = cache_choices 
    287  
    288     def __iter__(self): 
    289         if self.empty_label is not None: 
    290             yield (u"", self.empty_label) 
    291         for obj in self.queryset: 
    292             yield (obj.pk, smart_unicode(obj)) 
    293         # Clear the QuerySet cache if required. 
    294         if not self.cache_choices: 
    295             self.queryset._result_cache = None 
    296  
    297 class ModelChoiceField(ChoiceField): 
    298     """A ChoiceField whose choices are a model QuerySet.""" 
    299     # This class is a subclass of ChoiceField for purity, but it doesn't 
    300     # actually use any of ChoiceField's implementation. 
    301     default_error_messages = { 
    302         'invalid_choice': _(u'Select a valid choice. That choice is not one of' 
    303                             u' the available choices.'), 
    304     } 
    305  
    306     def __init__(self, queryset, empty_label=u"---------", cache_choices=False, 
    307                  required=True, widget=Select, label=None, initial=None, 
    308                  help_text=None, *args, **kwargs): 
    309         self.empty_label = empty_label 
    310         self.cache_choices = cache_choices 
    311         # Call Field instead of ChoiceField __init__() because we don't need 
    312         # ChoiceField.__init__(). 
    313         Field.__init__(self, required, widget, label, initial, help_text, 
    314                        *args, **kwargs) 
    315         self.queryset = queryset 
    316  
    317     def _get_queryset(self): 
    318         return self._queryset 
    319  
    320     def _set_queryset(self, queryset): 
    321         self._queryset = queryset 
    322         self.widget.choices = self.choices 
    323  
    324     queryset = property(_get_queryset, _set_queryset) 
    325  
    326     def _get_choices(self): 
    327         # If self._choices is set, then somebody must have manually set 
    328         # the property self.choices. In this case, just return self._choices. 
    329         if hasattr(self, '_choices'): 
    330             return self._choices 
    331         # Otherwise, execute the QuerySet in self.queryset to determine the 
    332         # choices dynamically. Return a fresh QuerySetIterator that has not 
    333         # been consumed. Note that we're instantiating a new QuerySetIterator 
    334         # *each* time _get_choices() is called (and, thus, each time 
    335         # self.choices is accessed) so that we can ensure the QuerySet has not 
    336         # been consumed. 
    337         return QuerySetIterator(self.queryset, self.empty_label, 
    338                                 self.cache_choices) 
    339  
    340     def _set_choices(self, value): 
    341         # This method is copied from ChoiceField._set_choices(). It's necessary 
    342         # because property() doesn't allow a subclass to overwrite only 
    343         # _get_choices without implementing _set_choices. 
    344         self._choices = self.widget.choices = list(value) 
    345  
    346     choices = property(_get_choices, _set_choices) 
    347  
    348     def clean(self, value): 
    349         Field.clean(self, value) 
    350         if value in EMPTY_VALUES: 
    351             return None 
    352         try: 
    353             value = self.queryset.get(pk=value) 
    354         except self.queryset.model.DoesNotExist: 
    355             raise ValidationError(self.error_messages['invalid_choice']) 
    356         return value 
    357  
    358 class ModelMultipleChoiceField(ModelChoiceField): 
    359     """A MultipleChoiceField whose choices are a model QuerySet.""" 
    360     hidden_widget = MultipleHiddenInput 
    361     default_error_messages = { 
    362         'list': _(u'Enter a list of values.'), 
    363         'invalid_choice': _(u'Select a valid choice. %s is not one of the' 
    364                             u' available choices.'), 
    365     } 
    366  
    367     def __init__(self, queryset, cache_choices=False, required=True, 
    368                  widget=SelectMultiple, label=None, initial=None, 
    369                  help_text=None, *args, **kwargs): 
    370         super(ModelMultipleChoiceField, self).__init__(queryset, None, 
    371             cache_choices, required, widget, label, initial, help_text, 
    372             *args, **kwargs) 
    373  
    374     def clean(self, value): 
    375         if self.required and not value: 
    376             raise ValidationError(self.error_messages['required']) 
    377         elif not self.required and not value: 
    378             return [] 
    379         if not isinstance(value, (list, tuple)): 
    380             raise ValidationError(self.error_messages['list']) 
    381         final_values = [] 
    382         for val in value: 
    383             try: 
    384                 obj = self.queryset.get(pk=val) 
    385             except self.queryset.model.DoesNotExist: 
    386                 raise ValidationError(self.error_messages['invalid_choice'] % val) 
    387             else: 
    388                 final_values.append(obj) 
    389         return final_values 
    390  
    391 # Model-FormSet integration ################################################### 
    392  
    393 def initial_data(instance, fields=None): 
    394     """ 
    395     Return a dictionary from data in ``instance`` that is suitable for 
    396     use as a ``Form`` constructor's ``initial`` argument. 
    397  
    398     Provide ``fields`` to specify the names of specific fields to return. 
    399     All field values in the instance will be returned if ``fields`` is not 
    400     provided. 
    401     """ 
    402     # avoid a circular import 
    403     from django.db.models.fields.related import ManyToManyField 
    404     opts = instance._meta 
    405     initial = {} 
    406     for f in opts.fields + opts.many_to_many: 
    407         if not f.editable: 
    408             continue 
    409         if fields and not f.name in fields: 
    410             continue 
    411         if isinstance(f, ManyToManyField): 
    412             # MultipleChoiceWidget needs a list of ints, not object instances. 
    413             initial[f.name] = [obj.pk for obj in f.value_from_object(instance)] 
    414         else: 
    415             initial[f.name] = f.value_from_object(instance) 
    416     return initial 
    417  
    418 class BaseModelFormSet(BaseFormSet): 
    419     """ 
    420     A ``FormSet`` for editing a queryset and/or adding new objects to it. 
    421     """ 
    422     model = None 
    423     queryset = None 
    424  
    425     def __init__(self, qs, data=None, files=None, auto_id='id_%s', prefix=None): 
    426304        kwargs = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 
    427         self.queryset = qs 
    428         kwargs['initial'] = [initial_data(obj) for obj in qs] 
     305        kwargs['initial'] = [model_to_dict(obj) for obj in self.get_queryset()] 
    429306        super(BaseModelFormSet, self).__init__(**kwargs) 
     307 
     308    def get_queryset(self): 
     309        if self.queryset is not None: 
     310            return self.queryset 
     311        return self.model._default_manager.get_query_set() 
    430312 
    431313    def save_new(self, form, commit=True): 
     
    433315        return save_instance(form, self.model(), commit=commit) 
    434316 
    435     def save_instance(self, form, instance, commit=True): 
     317    def save_existing(self, form, instance, commit=True): 
    436318        """Saves and returns an existing model instance for the given form.""" 
    437319        return save_instance(form, instance, commit=commit) 
     
    444326 
    445327    def save_existing_objects(self, commit=True): 
    446         if not self.queryset
     328        if not self.get_queryset()
    447329            return [] 
    448330        # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk 
    449331        existing_objects = {} 
    450         for obj in self.queryset
     332        for obj in self.get_queryset()
    451333            existing_objects[obj.pk] = obj 
    452334        saved_instances = [] 
    453         for form in self.change_forms: 
     335        for form in self.initial_forms: 
    454336            obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]] 
    455             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
     337            if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: 
    456338                obj.delete() 
    457339            else: 
    458                 saved_instances.append(self.save_instance(form, obj, commit=commit)) 
     340                saved_instances.append(self.save_existing(form, obj, commit=commit)) 
    459341        return saved_instances 
    460342 
    461343    def save_new_objects(self, commit=True): 
    462344        new_objects = [] 
    463         for form in self.add_forms: 
    464             if form.is_empty(): 
     345        for form in self.extra_forms: 
     346            if not form.has_changed(): 
    465347                continue 
    466348            # If someone has marked an add form for deletion, don't save the 
    467349            # object. At some point it would be nice if we didn't display 
    468350            # the deletion widget for add forms. 
    469             if self.deletable and form.cleaned_data[DELETION_FIELD_NAME]: 
     351            if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]: 
    470352                continue 
    471353            new_objects.append(self.save_new(form, commit=commit)) 
     
    478360        super(BaseModelFormSet, self).add_fields(form, index) 
    479361 
    480 def formset_for_model(model, form=BaseForm, formfield_callback=lambda f: f.formfield(), 
    481                       formset=BaseModelFormSet, extra=1, orderable=False, deletable=False, fields=None): 
    482     """ 
    483     Returns a FormSet class for the given Django model class. This FormSet 
    484     will contain change forms for every instance of the given model as well 
    485     as the number of add forms specified by ``extra``. 
    486      
    487     This is essentially the same as ``formset_for_queryset``, but automatically 
    488     uses the model's default manager to determine the queryset. 
    489     """ 
    490     form = form_for_model(model, form=form, fields=fields, formfield_callback=formfield_callback) 
    491     FormSet = formset_for_form(form, formset, extra, orderable, deletable) 
     362# XXX: Use at your own risk. This API *will* change. 
     363def _modelformset_factory(model, form=BaseModelForm, formfield_callback=lambda f: f.formfield(), 
     364                          formset=BaseModelFormSet, 
     365                          extra=1, can_delete=False, can_order=False, 
     366                          fields=None, exclude=None): 
     367    """ 
     368    Returns a FormSet class for the given Django model class. 
     369    """ 
     370    form = _modelform_factory(model, form=form, fields=fields, exclude=exclude, 
     371                              formfield_callback=formfield_callback) 
     372    FormSet = _formset_factory(form, formset, extra=extra, can_order=can_order, can_delete=can_delete) 
    492373    FormSet.model = model 
    493374&nbs