Opened 10 years ago
Closed 10 years ago
#24502 closed Uncategorized (needsinfo)
Using non model fields in an admin ModelForm does not work if the field is listed in fieldsets (and fields I think)
Reported by: | Benjamin Dauvergne | Owned by: | nobody |
---|---|---|---|
Component: | Uncategorized | Version: | 1.7 |
Severity: | Normal | Keywords: | |
Cc: | Triage Stage: | Unreviewed | |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
I have such change form:
19 class UserAttributeFormMixin(object): 20 def __init__(self, *args, **kwargs): 21 super(UserAttributeFormMixin, self).__init__(*args, **kwargs) 22 self.attributes = self.get_attributes() 23 initial = {} 24 if 'instance' in kwargs: 25 content_type = ContentType.objects.get_for_model(self.instance) 26 for av in models.AttributeValue.objects.filter( 27 content_type=content_type, 28 object_id=self.instance.pk): 29 initial[av.attribute.name] = av.to_python() 30 for attribute in self.attributes: 31 iv = initial.get(attribute.name) 32 attribute.contribute_to_form(self, initial=iv) 33 class UserChangeForm(forms.UserAttributeFormMixin, 11 AuthUserChangeForm): 12 13 class Meta(AuthUserChangeForm.Meta): 14 model = get_user_model() 15 fields = '__all__' 16
Use by the following ModelAdmin:
161 class AuthenticUserAdmin(UserAdmin): 162 fieldsets = ( 163 (None, {'fields': ('username', 'password')}), 164 (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), 165 (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 166 'groups')}), 167 (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 168 ) 169 form = admin_forms.UserChangeForm 170 add_form = admin_forms.UserCreationForm 171 add_fieldsets = ( 172 (None, { 173 'classes': ('wide',), 174 'fields': ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')} 175 ), 176 ) 177 list_filter = UserAdmin.list_filter + (UserRealmListFilter,ExternalUserListFilter) 178 179 def get_fieldsets(self, request, obj=None): 180 fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj)) 181 if obj: 182 if not request.user.is_superuser: 183 fieldsets[2][1]['fields'] = filter(lambda x: x != 184 'is_superuser', fieldsets[2][1]['fields']) 185 qs = models.Attribute.objects.all() 186 insertion_idx = 2 187 else: 188 qs = models.Attribute.objects.filter(required=True) 189 insertion_idx = 1 190 if qs.exists(): 191 fieldsets = list(fieldsets) 192 fieldsets.insert(insertion_idx,· 193 (_('Attributes'), {'fields': [at.name for at in qs]})) 194 return fieldsets
I get the following traceback I did not get with Django 1.5 (I cannot say if it was already the case with Django 1.6 we skiped it):
/home/bdauvergne/.virtualenvs/authentic/local/lib/python2.7/site-packages/django/contrib/admin/options.py in changeform_view 'name': force_text(opts.verbose_name), 'key': escape(object_id)}) if request.method == 'POST' and "_saveasnew" in request.POST: return self.add_view(request, form_url=reverse('admin:%s_%s_add' % ( opts.app_label, opts.model_name), current_app=self.admin_site.name)) ModelForm = self.get_form(request, obj) ... if request.method == 'POST': form = ModelForm(request.POST, request.FILES, instance=obj) if form.is_valid(): form_validated = True new_object = self.save_form(request, form, change=not add) else: ▶ Local vars /home/bdauvergne/.virtualenvs/authentic/local/lib/python2.7/site-packages/django/contrib/auth/admin.py in get_form """ Use special form during user creation """ defaults = {} if obj is None: defaults['form'] = self.add_form defaults.update(kwargs) return super(UserAdmin, self).get_form(request, obj, **defaults) ... def get_urls(self): from django.conf.urls import patterns return patterns('', (r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password)) ▶ Local vars /home/bdauvergne/.virtualenvs/authentic/local/lib/python2.7/site-packages/django/contrib/admin/options.py in get_form if defaults['fields'] is None and not modelform_defines_fields(defaults['form']): defaults['fields'] = forms.ALL_FIELDS try: return modelform_factory(self.model, **defaults) except FieldError as e: raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.' % (e, self.__class__.__name__)) ... def get_changelist(self, request, **kwargs): """ Returns the ChangeList class for use on the changelist page. """ from django.contrib.admin.views.main import ChangeList ▶ Local vars
What happens is that ModelAdmin is passing all fields defined to modelform_factory which use them to set the Meta.fields property of a new modelform class and it breaks with a new validation of corrects Meta.fields in django/forms/models.py l.294:
280 281 fields = fields_for_model(opts.model, opts.fields, opts.exclude, 282 opts.widgets, formfield_callback, 283 opts.localized_fields, opts.labels, 284 opts.help_texts, opts.error_messages) 285 286 # make sure opts.fields doesn't specify an invalid field 287 none_model_fields = [k for k, v in six.iteritems(fields) if not v] 288 missing_fields = (set(none_model_fields) - 289 set(new_class.declared_fields.keys())) 290 if missing_fields: 291 message = 'Unknown field(s) (%s) specified for %s' 292 message = message % (', '.join(missing_fields), 293 opts.model.__name__) 294 raise FieldError(message) 295 # Override default model fields with any custom declared ones 296 # (plus, include all the other declared fields). 297 fields.update(new_class.declared_fields) 298 else:
Don't know it it's better to allow non-model fields in Meta.fields or to fix the admin.
Change History (2)
comment:1 by , 10 years ago
comment:2 by , 10 years ago
Resolution: | → needsinfo |
---|---|
Status: | new → closed |
Your example is too complicated for me to debug and reproduce (for example attribute.contribute_to_form
isn't part of Django), and there's evidence that referring to non-model fields does work, see https://github.com/django/django/commit/dc3d2ac98c1bcfad74d3e9523caf07e7e9fb15aa for tests. Please reopen if you can simplify your example so it can be reproduced (ideally, as a test for Django's test suite that passes on 1.5 and fails in 1.7). If you can bisect the regression all the better.
I worked around the problem with this hack in my code: