My site supports multiple languages. I have an object (Category) that should have 1 or many translation objects (CategoryTranslation) respective to supported languages (they are not so simple, so, please, don't advice to use translation apps). Besides, dependent objects have link to user that creates and last updates them.
I want following in my Category Admin panel:
Category fields.
Inlined CategoryTranslation object initialized by existing languages. For example, in my site supports Russian and English, 2 (and only 2) inlines should be shown, first should have non-editable field language equals to 'ru', second - 'en'.
After Category save, new CategoryTranslation should get created_by field value equals to current user, edited CategoryTranslation should get edited_by field value equals to current user.
Here is what I have done for now:
class CategoryTranslationFormSet(BaseInlineFormSet):
def total_form_count(self):
return len(settings.LANGUAGES)
def _construct_form(self, i, **kwargs):
form = super(CategoryTranslationFormSet, self)._construct_form(i, **kwargs)
form.initial['language_code'] = settings.LANGUAGES[i][0]
return form
def save(self, commit=True):
translations = super(CategoryTranslationFormSet, self).save(commit=False)
for translation in translations:
if not translation.id:
translation.created_by = self.user
translation.edited_by = self.user
translation.save()
class CategoryTranslationAdmin(admin.TabularInline):
model = CategoryTranslation
formset = CategoryTranslationFormSet
max_num = 0
fields = ('title', 'description', 'language_code', 'created_by', 'created', 'edited_by', 'edited')
readonly_fields = ('created_by', 'created', 'edited_by', 'edited')
class CategoryAdmin(tree_editor.TreeEditor):
fieldsets = (
(None, {'fields': ('parent', 'is_list', 'is_active')}),
(_('Audit'), {'fields': ('created_by', 'created', 'edited_by', 'edited'),
'classes': ('collapse',)
}),
)
readonly_fields = ('created_by', 'created', 'edited_by', 'edited')
list_display = ('is_list', 'is_active')
inlines = [CategoryTranslationAdmin]
def save_model(self, request, obj, form, change):
add_user_for_audit(request, obj, change)
super(CategoryAdmin, self).save_model(request, obj, form, change)
def save_formset(self, request, form, formset, change):
formset.user = request.user
super(CategoryAdmin, self).save_formset(request, form, formset, change)
As you can see, max_num field of CategoryTranslationAdmin is 0 to prevent adding of additional translations, total_form_count method of form set returns count of supported langauges and forms initialized with available languages on form creation.
Update
I also set current user to formset in save_formset(self, request, form, formset, change) method and use him to populate fields of changed objects in save() method of CategoryTranslationFormSet class.
Is it correct?
So now I need:
Make language_code field read_only (if I simply add it to readonly_fields tuple, I will not be able to initialize it in form).
Understand, is my solution with appending user before save is correct.
Check this one:
from django.forms.widgets import HiddenInput
def _construct_form(self, i, **kwargs):
form = super(CategoryTranslationFormSet, self)._construct_form(i, **kwargs)
form.empty_permitted = False # Force the translation
if 'language_code' not in form.initial.keys():
form.initial['language_code'] = settings.LANGUAGES[i][0]
for k in form.fields.keys():
lang = LANG_CHOICES[zip(*settings.LANGUAGES)[0].index(form.initial['language_code'])][1]
if k == 'language':
form.fields[k].widget = HiddenInput()
form.fields[k].label = u'%s %s' % (form.fields[k].label, lang)
else:
form.fields[k].label = u'%s (%s)' % (form.fields[k].label, lang)
return form
Related
When saving the formset, it is not validating and respecting the model fields that I have made unique and therefore renders an Integrity Error should I duplicate fields deliberately to test it.
My guess is that the formsets themselves validate correctly, but as the unique field is something I have to assign during the save(commit=False) process it never gets validated. Does that make sense?
Is there something I am missing please?
My code:
class ClientCreate(LoginRequiredMixin, FormView):
def dispatch(self, *args, **kwargs):
self.case = Case.objects.get(pk=kwargs['case_pk'])
self.num_clients = self.case.number_clients
return super().dispatch(*args, **kwargs)
template_name = 'clients/client_form.html'
form_class = modelformset_factory(Client, ClientForm,
min_num=2, max_num=2, extra=0,
validate_max=True, validate_min=True,
can_delete=False)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["queryset"] = Client.objects.none()
return kwargs
def form_valid(self, form_class):
form_class.save()
return super().form_valid(form)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if self.request.POST:
ctx['inlines'] = self.form_class(self.request.POST)
else:
ctx['inlines'] = self.form_class()
return ctx
def get_success_url(self):
return reverse('client-list',
kwargs={'case_pk': self.kwargs['case_pk']})
I appreciate that formview is really supposed to be used for single form saving, but this does actually work correctly when not having duplicate unique items.
Many thanks
EDIT:::
This is my function based version that also suffers from the same issue:
#login_required
def client(request, case_pk):
template_name = 'clients/client_form.html'
case = get_object_or_404(Case,
pk=case_pk, adviser__company__account=request.user
)
formset_class = modelformset_factory(Client, ClientForm,
min_num=case.number_clients,
max_num=case.number_clients, extra=0,
validate_max=True, validate_min=True,
can_delete=False)
formset = formset_class(request.POST or None)
if request.method == 'POST':
# check all formsets valid
if all(form.is_valid() for form in formset):
for f in formset:
if f.is_valid():
form = f.save(commit=False)
form.case = case
f.save()
return HttpResponseRedirect(reverse('client-create',
kwargs={'case_pk': case_pk}))
return render(request, template_name, {
'inlines': formset,
'case': case,
'breadcrumbs': 'Family & Dependants'
})
Client Form:
class ClientForm(ModelForm):
class Meta:
model = Client
fields = ['prefix', 'first_name', 'middle_names', 'last_name',
'gender', 'date_of_birth', 'residence', 'address_1',
'address_2', 'address_3', 'city', 'postcode', 'telephone',
'marital_status', 'widowed_date_of_death',
'have_will', 'why_changing', 'existing_poa', 'dependant', ]
Client Model is large so this is the unique clause:
class Meta:
unique_together = ('case', 'first_name', 'last_name',
'date_of_birth', )
modelformset_factory appears to not respect database level constraints. Therefore, the check has to be done before saving the form.
They way to do this is by overriding the BaseModelFormSet class.
Solution here: Save multiple objects with a unique attribute using Django formset
class Book(models.Model):
title = models.CharField(..., null=True)
type = models.CharField(...)
author = models.CharField(...)
I have a simple class in models.py. In admin I would like to hide title of the book (in book details form) when type of the saved book is 1.
How do this in a simplest way?
For Django > 1.8 one can directly set the fields to be excluded in admin:
class PostCodesAdmin(admin.ModelAdmin):
exclude = ('pcname',)
Hidden fields are directly defined in Django's ORM by setting the Field attribute: editable = False
e.g.
class PostCodes(models.Model):
gisid = models.IntegerField(primary_key=True)
pcname = models.CharField(max_length=32, db_index=True, editable=False)
...
However, setting or changing the model's fields directly may not always be possible or advantegous. In principle the following admin.py setup could work, but won't since exclude is an InlineModelAdmin option.
class PostCodesAdmin(admin.ModelAdmin):
exclude = ('pcname',)
....
A solution working at least in Django 1.4 (and likely later version numbers) is:
class PostCodesAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(PostCodesAdmin, self).get_form(request, obj, **kwargs)
del form.base_fields['enable_comments']
return form
For the admin list-view of the items, it suffices to simply leave out fields not required:
e.g.
class PostCodesAdmin(admin.ModelAdmin):
list_display = ('id', 'gisid', 'title', )
You are to create admin.py in your module (probably book)
class BookAdmin(admin.ModelAdmin):
list_display = ("pk", "get_title_or_nothing")
In Book class:
class Book:
...
def get_title_or_nothing(self):
if self.type == WEIRD_TYPE:
return ""
return self.title
UPDATED:
class BookAdmin(admin.ModelAdmin):
list_display = ("pk", "get_title_or_nothing")
def get_form(self, request, obj=None, **kwargs):
if obj.type == "1":
self.exclude = ("title", )
form = super(BookAdmin, self).get_form(request, obj, **kwargs)
return form
I tried to override get_form() function but some mix up errors occur when I switch in different records. I found there is a get_exclude() function we can override.
Use:
class BookAdmin(admin.ModelAdmin):
def get_exclude(self, request, obj=None):
if obj and obj.type == "1":
# When you create new data the obj is None
return ("title", )
return super().get_exclude(request, obj)
class BookAdmin(admin.ModelAdmin):
exclude = ("fieldname",) # hide fields which you want
Apropos #Lorenz #mrts answer
with Django 2.1 I found that exclude does not work if the field is already specified via fields = .
In that case you may use
self.fields.remove('title')
fields will have to be defined as a list [] for this to work
If you want to maintain the value in the form (for example set a value, i.e. user, based on the request) and hide the field, you can change the widget to forms.HiddenInput():
from django import forms
...
def get_form(self, request, obj=None, **kwargs):
"""Set defaults based on request user"""
# update user field with logged user as default
form = super().get_form(request, obj, **kwargs)
form.base_fields["user"].initial = request.user.id
form.base_fields["user"].widget = forms.HiddenInput()
return form
Here is a working example
class BookAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj):
if obj is None:
return [
(
None,
{'fields': ('type', 'description',)}
)
]
elif request.user.is_superuser:
return [
(
None,
{'fields': ('type', 'status', 'author', 'store', 'description',)}
)
]
else:
return [
(
None,
{'fields': ('type', 'date', 'author', 'store',)}
)
]
I've tried various methods to achieve this.
I decided against overriding formfield_for_dbfield as it doesn't get a copy of the request object and I was hoping to avoid the thread_locals hack.
I settled on overriding get_form in my ModelAdmin class and tried the following:
class PageOptions(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.fieldsets = ((None, {'fields': ('title','name',),}),)
else:
self.fieldsets = ((None, {'fields': ('title',),}),)
return super(PageOptions,self).get_form(request, obj=None, **kwargs)
When I print fieldsets or declared_fieldsets from within get_form I get None (or whatever I set as an initial value in PageOptions).
Why doesn't this work and is there a better way to do this?
I have some sample code from a recent project of mine that I believe may help you. In this example, super users can edit every field, while everyone else has the "description" field excluded.
Note that I think it's expected that you return a Form class from get_form, which could be why yours was not working quite right.
Here's the example:
class EventForm(forms.ModelForm):
class Meta:
model = models.Event
exclude = ['description',]
class EventAdminForm(forms.ModelForm):
class Meta:
model = models.Event
class EventAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
return EventAdminForm
else:
return EventForm
admin.site.register(models.Event, EventAdmin)
I have no idea why printing the property doesn't give you want you just assigned (I guess may be that depends on where you print, exactly), but try overriding get_fieldsets instead. The base implementation looks like this:
def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
form = self.get_formset(request).form
return [(None, {'fields': form.base_fields.keys()})]
I.e. you should be able to just return your tuples.
EDIT by andybak. 4 years on and I found my own question again when trying to do something similar on another project. This time I went with this approach although modified slightly to avoid having to repeat fieldsets definition:
def get_fieldsets(self, request, obj=None):
# Add 'item_type' on add forms and remove it on changeforms.
fieldsets = super(ItemAdmin, self).get_fieldsets(request, obj)
if not obj: # this is an add form
if 'item_type' not in fieldsets[0][1]['fields']:
fieldsets[0][1]['fields'] += ('item_type',)
else: # this is a change form
fieldsets[0][1]['fields'] = tuple(x for x in fieldsets[0][1]['fields'] if x!='item_type')
return fieldsets
This is my solution:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.exclude = ()
else:
self.exclude = ('field_to_exclude',)
return super(MyModelAdmin, self).get_form(request, obj=None, **kwargs)
Hope can help
For creating customized admin forms we have defined a new class which can be used as mixin. The approach is quite flexible:
ModelAdmin: define a fieldset containing all fields
ModelForm: narrow the fields being shown
FlexibleModelAdmin: overriding get_fieldsets-method of ModelAdmin; returns a reduced fieldset that only contains the fields defined in the admin form
class FlexibleModelAdmin(object):
'''
adds the possibility to use a fieldset as template for the generated form
this class should be used as mix-in
'''
def _filterFieldset(self, proposed, form):
'''
remove fields from a fieldset that do not
occur in form itself.
'''
allnewfields = []
fields = form.base_fields.keys()
fieldset = []
for fsname, fdict in proposed:
newfields = []
for field in fdict.get('fields'):
if field in fields:
newfields.append(field)
allnewfields.extend(newfields)
if newfields:
newentry = {'fields': newfields}
fieldset.append([fsname, newentry])
# nice solution but sets are not ordered ;)
# don't forget fields that are in a form but were forgotten
# in fieldset template
lostfields = list(set(fields).difference(allnewfields))
if len(lostfields):
fieldset.append(['lost in space', {'fields': lostfields}])
return fieldset
def get_fieldsets(self, request, obj=None):
'''
Hook for specifying fieldsets for the add form.
'''
if hasattr(self, 'fieldsets_proposed'):
form = self.get_form(request, obj)
return self._filterFieldset(self.fieldsets_proposed, form)
else:
return super(FlexibleModelAdmin, self).get_fieldsets(request, obj)
In the admin model you define fieldsets_proposed which serves as template and contains all fields.
class ReservationAdmin(FlexibleModelAdmin, admin.ModelAdmin):
list_display = ['id', 'displayFullName']
list_display_links = ['id', 'displayFullName']
date_hierarchy = 'reservation_start'
ordering = ['-reservation_start', 'vehicle']
exclude = ['last_modified_by']
# considered by FlexibleModelAdmin as template
fieldsets_proposed = (
(_('General'), {
'fields': ('vehicle', 'reservation_start', 'reservation_end', 'purpose') # 'added_by'
}),
(_('Report'), {
'fields': ('mileage')
}),
(_('Status'), {
'fields': ('active', 'editable')
}),
(_('Notes'), {
'fields': ('note')
}),
)
....
def get_form(self, request, obj=None, **kwargs):
'''
set the form depending on the role of the user for the particular group
'''
if request.user.is_superuser:
self.form = ReservationAdminForm
else:
self.form = ReservationUserForm
return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(Reservation, ReservationAdmin)
In your model forms you can now define the fields to be excluded/included. get_fieldset() of the mixin-class makes sure that only the fields defined in the form are being returned.
class ReservationAdminForm(ModelForm):
class Meta:
model = Reservation
exclude = ('added_by', 'last_modified_by')
class ReservationUserForm(BaseReservationForm):
class Meta:
model = Reservation
fields = ('vehicle', 'reservation_start', 'reservation_end', 'purpose', 'note')
Don't change the value of self attributes because it's not thread-safe. You need to use whatever hooks to override those values.
In my case, with Django 2.1 you could do the following
In forms.py
class ObjectAddForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
class ObjectChangeForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
And then in the admin.py
from your.app import ObjectAddForm, ObjectChangeForm
class ObjectAdmin(admin.ModelAdmin):
....
def get_form(self, request, obj=None, **kwargs):
if obj is None:
kwargs['form'] = ObjectAddForm
else:
kwargs['form'] = ObjectChangeForm
return super().get_form(request, obj, **kwargs)
You can use get_fields or get_fieldset methods for that purpose
You could make fieldsetsand form properties and have them emit signals to get the desired forms/fieldsets.
My admin looks like this (with no exclude variable):
class MovieAdmin(models.ModelAdmin)
fields = ('name', 'slug', 'imdb_link', 'start', 'finish', 'added_by')
list_display = ('name', 'finish', 'added_by')
list_filter = ('finish',)
ordering = ('-finish',)
prepopulated_fields = {'slug': ('name',)}
form = MovieAdminForm
def get_form(self, request, obj=None, **kwargs):
form = super(MovieAdmin, self).get_form(request, obj, **kwargs)
form.current_user = request.user
return form
admin.site.register(Movie, MovieAdmin)
The form:
class MovieAdminForm(forms.ModelForm):
class Meta:
model = Movie
def save(self, commit=False):
instance = super(MovieAdminForm, self).save(commit=commit)
if not instance.pk and not self.current_user.is_superuser:
if not self.current_user.profile.is_manager:
instance.added_by = self.current_user.profile
instance.save()
return instance
I'm trying to remove the added_by field for users since I'd prefer to populate that from the session. I've tried methods from the following:
Django admin - remove field if editing an object
Remove fields from ModelForm
http://www.mdgart.com/2010/04/08/django-admin-how-to-hide-fields-in-a-form-for-certain-users-that-are-not-superusers/
However with each one I get: KeyError while rendering: Key 'added_by' not found in Form. It seems I need to remove the field earlier in the form rendering process but I'm stuck on where to do this.
So how can I exclude the added_by field for normal users?
You're probably getting that error when list_display is evaluated. You can't show a field that's excluded. The version with added_by removed also needs a corresponding list_display.
def get_form(self, request, obj=None, **kwargs):
current_user = request.user
if not current_user.profile.is_manager:
self.exclude = ('added_by',)
self.list_display = ('name', 'finish')
form = super(MovieAdmin, self).get_form(request, obj, **kwargs)
form.current_user = current_user
return form
I've tried various methods to achieve this.
I decided against overriding formfield_for_dbfield as it doesn't get a copy of the request object and I was hoping to avoid the thread_locals hack.
I settled on overriding get_form in my ModelAdmin class and tried the following:
class PageOptions(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.fieldsets = ((None, {'fields': ('title','name',),}),)
else:
self.fieldsets = ((None, {'fields': ('title',),}),)
return super(PageOptions,self).get_form(request, obj=None, **kwargs)
When I print fieldsets or declared_fieldsets from within get_form I get None (or whatever I set as an initial value in PageOptions).
Why doesn't this work and is there a better way to do this?
I have some sample code from a recent project of mine that I believe may help you. In this example, super users can edit every field, while everyone else has the "description" field excluded.
Note that I think it's expected that you return a Form class from get_form, which could be why yours was not working quite right.
Here's the example:
class EventForm(forms.ModelForm):
class Meta:
model = models.Event
exclude = ['description',]
class EventAdminForm(forms.ModelForm):
class Meta:
model = models.Event
class EventAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
return EventAdminForm
else:
return EventForm
admin.site.register(models.Event, EventAdmin)
I have no idea why printing the property doesn't give you want you just assigned (I guess may be that depends on where you print, exactly), but try overriding get_fieldsets instead. The base implementation looks like this:
def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
form = self.get_formset(request).form
return [(None, {'fields': form.base_fields.keys()})]
I.e. you should be able to just return your tuples.
EDIT by andybak. 4 years on and I found my own question again when trying to do something similar on another project. This time I went with this approach although modified slightly to avoid having to repeat fieldsets definition:
def get_fieldsets(self, request, obj=None):
# Add 'item_type' on add forms and remove it on changeforms.
fieldsets = super(ItemAdmin, self).get_fieldsets(request, obj)
if not obj: # this is an add form
if 'item_type' not in fieldsets[0][1]['fields']:
fieldsets[0][1]['fields'] += ('item_type',)
else: # this is a change form
fieldsets[0][1]['fields'] = tuple(x for x in fieldsets[0][1]['fields'] if x!='item_type')
return fieldsets
This is my solution:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if request.user.is_superuser:
self.exclude = ()
else:
self.exclude = ('field_to_exclude',)
return super(MyModelAdmin, self).get_form(request, obj=None, **kwargs)
Hope can help
For creating customized admin forms we have defined a new class which can be used as mixin. The approach is quite flexible:
ModelAdmin: define a fieldset containing all fields
ModelForm: narrow the fields being shown
FlexibleModelAdmin: overriding get_fieldsets-method of ModelAdmin; returns a reduced fieldset that only contains the fields defined in the admin form
class FlexibleModelAdmin(object):
'''
adds the possibility to use a fieldset as template for the generated form
this class should be used as mix-in
'''
def _filterFieldset(self, proposed, form):
'''
remove fields from a fieldset that do not
occur in form itself.
'''
allnewfields = []
fields = form.base_fields.keys()
fieldset = []
for fsname, fdict in proposed:
newfields = []
for field in fdict.get('fields'):
if field in fields:
newfields.append(field)
allnewfields.extend(newfields)
if newfields:
newentry = {'fields': newfields}
fieldset.append([fsname, newentry])
# nice solution but sets are not ordered ;)
# don't forget fields that are in a form but were forgotten
# in fieldset template
lostfields = list(set(fields).difference(allnewfields))
if len(lostfields):
fieldset.append(['lost in space', {'fields': lostfields}])
return fieldset
def get_fieldsets(self, request, obj=None):
'''
Hook for specifying fieldsets for the add form.
'''
if hasattr(self, 'fieldsets_proposed'):
form = self.get_form(request, obj)
return self._filterFieldset(self.fieldsets_proposed, form)
else:
return super(FlexibleModelAdmin, self).get_fieldsets(request, obj)
In the admin model you define fieldsets_proposed which serves as template and contains all fields.
class ReservationAdmin(FlexibleModelAdmin, admin.ModelAdmin):
list_display = ['id', 'displayFullName']
list_display_links = ['id', 'displayFullName']
date_hierarchy = 'reservation_start'
ordering = ['-reservation_start', 'vehicle']
exclude = ['last_modified_by']
# considered by FlexibleModelAdmin as template
fieldsets_proposed = (
(_('General'), {
'fields': ('vehicle', 'reservation_start', 'reservation_end', 'purpose') # 'added_by'
}),
(_('Report'), {
'fields': ('mileage')
}),
(_('Status'), {
'fields': ('active', 'editable')
}),
(_('Notes'), {
'fields': ('note')
}),
)
....
def get_form(self, request, obj=None, **kwargs):
'''
set the form depending on the role of the user for the particular group
'''
if request.user.is_superuser:
self.form = ReservationAdminForm
else:
self.form = ReservationUserForm
return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
admin.site.register(Reservation, ReservationAdmin)
In your model forms you can now define the fields to be excluded/included. get_fieldset() of the mixin-class makes sure that only the fields defined in the form are being returned.
class ReservationAdminForm(ModelForm):
class Meta:
model = Reservation
exclude = ('added_by', 'last_modified_by')
class ReservationUserForm(BaseReservationForm):
class Meta:
model = Reservation
fields = ('vehicle', 'reservation_start', 'reservation_end', 'purpose', 'note')
Don't change the value of self attributes because it's not thread-safe. You need to use whatever hooks to override those values.
In my case, with Django 2.1 you could do the following
In forms.py
class ObjectAddForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
class ObjectChangeForm(forms.ModelForm):
class Meta:
model = Object
exclude = []
And then in the admin.py
from your.app import ObjectAddForm, ObjectChangeForm
class ObjectAdmin(admin.ModelAdmin):
....
def get_form(self, request, obj=None, **kwargs):
if obj is None:
kwargs['form'] = ObjectAddForm
else:
kwargs['form'] = ObjectChangeForm
return super().get_form(request, obj, **kwargs)
You can use get_fields or get_fieldset methods for that purpose
You could make fieldsetsand form properties and have them emit signals to get the desired forms/fieldsets.