I have complex user-defined permissions for my users. But to make thinks simpler, let's imagine there's only read-only or write permissions for each user.
I am using Django forms to edit and save model objects. And my goal is to render <input> in the Django HTML template for those users who have the permission to edit a given model instance, and a hard-coded data (without <input> tag) if the user has only read-only permission.
Currently, I have the following code in my Django template to achieve this:
{%if user.has_permission_to_edit %}
{{my_form.my_field}}
{% else %}
{{my_form.instance.my_field}}
{% endif %}
And here's my_form:
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control input-sm'
if field.required == True:
field.widget.attrs['required'] = ''
class Meta:
model = MyModel
fields = ('my_field',)
The problem with the code in the template is that I have to use multiple {% if %}{% else %} blocks. I am relatively new to Django, and I know that there is plethora of advanced tools making Django code potentially super DRY, so I want to asked you guys, what is the most DRY method to organize what I described in the template. Specifically, is there any way to make Django forms return instance values based on some condition specified inside the form definition? Or do I have to some used-defined tag ? Or maybe some totally different architecture is used to achieve such goals?
For what I understood form your question, you want to pass the instance of the data fetched from your data-source.
from .forms import MyForm
from django.shortcuts import render
assuming you have created a forms.py file at the views.py level.
Fetching data from data-source(Detail is the model in below example)
detail_instance = Detail.objects.get(user=request.user.id)
reg_form = MyForm(instance=detail_instance or None)
# In case of edit scenario you can pass in the post params to the form as well
reg_form = MyForm(request.POST, instance=detail_instance)
# Or form with uploads
reg_form = MyForm(request.POST, request.FILES, instance=detail_instance)
Now once we have data inside our reg_form parameter we can pass it in the template
return render(request, 'applicant/register.html', { 'my_form' : reg_form})
Do whatever you wish, with you my_form variable in the template.
BASED ON THE UPDATED QUESTION
You can pass parameter to the init function of a form
reg_form = MyForm(exist = exist, some_param = param_value, instance=detail_instance or None)
After passing the param, the param can be fetched and processed in the init function of the form
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
exist = kwargs.pop('exist', None)
pk_reg = kwargs.pop('param', None)
super(MyForm, self).__init__(*args, **kwargs)
#do some stuff with you custom params here
if exist == True or pk_reg:
self.fields['username'].widget.attrs['readonly'] = True
The above approach has an alternative as well for having separate forms for separate permission and calling the appropriate form based on the user permissions.
To achieve client-side validation making the user to fill out non-null fields before submitting, I use the following code:
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
if field.required == True:
field.widget.attrs['required'] = ''
This translates to the following html in the template:
<input class="form-control" ........ required="">
Now, when I use formsets, the required HTML attribute does not appear in the tempalte. The question is, how do I make Django formsets inherit this required attribute from the original forms - if it's possible whatsoever?
MyFormSet = modelformset_factory(MyModel, fields=(...))
formset = MyFormSet(queryset = MyModel.objects.filter(...))
How about creating formset from MyForm?
MyFormSet = forms.formset_factory(MyForm)
After spending three hours, I've solved the issue by setting a custom form in modelformset_factory. Maybe it will be useful for someone else
MyFormSet = modelformset_factory(MyModel, MyForm)
formset = MyFormSet(queryset = MyModel.objects.filter(...))
Specifying MyForm effectively tells Django to inherit all widget attributes that you have once declared in the MyForm definition.
Using formset_factory is for some reasons a headache for me, primarily because it accepts values instead of querysets which means I have to bother about foreign key relationships.
I use a variable in the base of my API url, identical to the setup found in the docs for Django REST Framework:
/api/<brand>/states/<state_pk>/
Everything after the base brand slug is a standard API format, and so I use ModelViewSets to generate all my list and detail views for my objects. Everything in the API is filtered by the brand, so this setup makes sense.
simplified project/urls.py
urlpatterns = patterns(
'',
url(r'^v2/(?P<brand_slug>\w+)/', include(router.urls, namespace='v2')),
)
simplified api/urls.py
router = routers.DefaultRouter()
router.register(r'states', StateViewSet)
router.register(r'cities', CityViewSet)
I also need hypermedia links for all models, and this is where I've run into problems. The REST framework doesn't know how to grab this brand variable and use it to generate correct links. Attempting to solve this problem by following the docs leaves me with 2 setbacks:
While the docs explain how to overwrite the HyperlinkRelatedField class, they never say where to put THAT class so that it works with my Serializers.
There's no mention on how to actually get the brand variable from the URL into the HyperlinkRelatedField class.
What are the missing elements here?
So, I figured it out.
Getting the URL variable into the Serializer
To do this, you need to overwrite the get_serializer_context() method for your ModelViewSet, and send in the variable from your kwargs
class BrandedViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
context['brand_slug'] = self.kwargs.get('brand_slug')
return context
Then, you can just extend all of your ModelViewSets with that class:
class StateViewSet(BrandedViewSet):
queryset = State.objects.all()
serializer_class = StateSerializer
What's nice is that even though you've injected the Serializer with this variable, it's ALSO accessible from the HyperlinkedRelatedField class, via self.context, and that's how the next part is possible.
Building a Custom Hypermedia link with extra URL variables
The docs were correct in overwriting get_url():
class BrandedHyperlinkMixin(object):
def get_url(self, obj, view_name, request, format):
""" Extract brand from url
"""
if hasattr(obj, 'pk') and obj.pk is None:
return None
lookup_value = getattr(obj, self.lookup_field)
kwargs = {self.lookup_url_kwarg: lookup_value}
kwargs['brand_slug'] = self.context['brand_slug']
return reverse(
view_name, kwargs=kwargs, request=request, format=format)
Except, you'll notice I'm grabbing the variable from the context I set in part 1. I was unable to get the context from the object as the docs suggested, and this method turned out to be simpler.
The reason it's a mixin is because we need to extend TWO classes for this to work on all the url hyperlinks and not just the related field hyperlinks.
class BrandedHyperlinkedIdentityField(BrandedHyperlinkMixin,
serializers.HyperlinkedIdentityField):
pass
class BrandedHyperlinkedRelatedField(BrandedHyperlinkMixin,
serializers.HyperlinkedRelatedField):
pass
class BrandedSerializer(serializers.HyperlinkedModelSerializer):
serializer_related_field = BrandedHyperlinkedRelatedField
serializer_url_field = BrandedHyperlinkedIdentityField
Now we can safely extend our serializer and the hyperlinks show the brand variable!
class StateSerializer(BrandedSerializer):
class Meta:
model = State
fields = ('url', 'slug', 'name', 'abbrev', )
I am writing a little app to allow an AddThis share field in the django admin change list to allow the user share the object they are currently editing (as well as seeing the share count):
Taking a simple BlogEntry as an example, I have created a custom ModelAdmin:
class AddThisAdmin(admin.ModelAdmin):
addthis_config = {
'title_field' : None,
'description_field' : None,
'url_field' : None,
'image_field' : None,
}
def get_form(self, request, obj=None, *args, **kwargs):
metaform = super(AddThisAdmin, self).get_form(request, obj, **kwargs)
if obj:
# Grab users config and find the fields they specified ...
metaform.base_fields['add_this'] = AddThisField(self.add_this)
return metaform
Which is inherited in the users BlogEntryAdmin like so:
class BlogEntryAdmin(admin.ModelAdmin, AddThisAdmin):
addthis_config = {
'title_field' : 'blog_title',
'description_field' : 'blurb',
}
where the addthis_config allows the user to specify the fields in their BlogEntry object from where to pull the title/description/url and image used in AddThis. This all works really nicely until I decide to use a custom fieldset in the BlogEntryAdmin:
class BlogEntryAdmin(admin.ModelAdmin, AddThisAdmin):
addthis_config = {
'title_field' : 'blog_title',
'description_field' : 'blurb',
}
fieldsets = [{ ... }]
'BlogEntry.fieldsets0['fields']' refers to field 'add_this' that is missing from the form.
I understand that this is happening because the django admin runs a validation on the fieldsets (django.contrib.admin.validation) on the BlogEntryAdmin class before it is actually instantiated (and my custom field is inserted).
tldr : Is there a way I can tell the django.contrib.admin.validation to ignore the field in the fieldset?
The typical approach is to provide base form like AddThisAdminForm which has the required field(s), and the make other ModelAdmin's forms inherit from that. It looks like you're trying to avoid that and auto insert the fields into whatever form is being used. If you insist on that approach, something like the following should work much better:
def get_form(self, request, obj=None, **kwargs):
ModelForm = super(AddThisAdmin, self).get_form(request, obj, **kwargs)
class AddThisForm(ModelForm):
add_this = AddThisField(self.add_this)
return AddThisForm
It's not documented, but you could use the get_fieldsets method to define your fieldsets. As an example, look at how Django changes the fieldsets in the UserAdmin when adding new users.
I've not tested this, but I believe it will avoid the fieldset validation.
I want to have additional fields regarding value of one field. Therefor I build a custom admin form to add some new fields.
Related to the blogpost of jacobian 1 this is what I came up with:
class ProductAdminForm(forms.ModelForm):
class Meta:
model = Product
def __init__(self, *args, **kwargs):
super(ProductAdminForm, self).__init__(*args, **kwargs)
self.fields['foo'] = forms.IntegerField(label="foo")
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
But the additional field 'foo' does not show up in the admin. If I add the field like this, all works fine but is not as dynamic as required, to add the fields regarding the value of another field of the model
class ProductAdminForm(forms.ModelForm):
foo = forms.IntegerField(label="foo")
class Meta:
model = Product
class ProductAdmin(admin.ModelAdmin):
form = ProductAdminForm
admin.site.register(Product, ProductAdmin)
So is there any initialize method that i have to trigger again to make the new field working? Or is there any other attempt?
Here is a solution to the problem. Thanks to koniiiik i tried to solve this by extending the *get_fieldsets* method
class ProductAdmin(admin.ModelAdmin):
def get_fieldsets(self, request, obj=None):
fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
fieldsets[0][1]['fields'] += ['foo']
return fieldsets
If you use multiple fieldsets be sure to add the to the right fieldset by using the appropriate index.
The accepted answer above worked in older versions of django, and that's how I was doing it. This has now broken in later django versions (I am on 1.68 at the moment, but even that is old now).
The reason it is now broken is because any fields within fieldsets you return from ModelAdmin.get_fieldsets() are ultimately passed as the fields=parameter to modelform_factory(), which will give you an error because the fields on your list do not exist (and will not exist until your form is instantiated and its __init__ is called).
In order to fix this, we must override ModelAdmin.get_form() and supply a list of fields that does not include any extra fields that will be added later. The default behavior of get_form is to call get_fieldsets() for this information, and we must prevent that from happening:
# CHOOSE ONE
# newer versions of django use this
from django.contrib.admin.utils import flatten_fieldsets
# if above does not work, use this
from django.contrib.admin.util import flatten_fieldsets
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
# add your dynamic fields here..
for fieldname in ('foo', 'bar', 'baz',):
self.fields[fieldname] = form.CharField()
class MyAdmin(ModelAdmin):
form = MyModelForm
fieldsets = [
# here you put the list of fieldsets you want displayed.. only
# including the ones that are not dynamic
]
def get_form(self, request, obj=None, **kwargs):
# By passing 'fields', we prevent ModelAdmin.get_form from
# looking up the fields itself by calling self.get_fieldsets()
# If you do not do this you will get an error from
# modelform_factory complaining about non-existent fields.
# use this line only for django before 1.9 (but after 1.5??)
kwargs['fields'] = flatten_fieldsets(self.declared_fieldsets)
# use this line only for django 1.9 and later
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
return super(MyAdmin, self).get_form(request, obj, **kwargs)
def get_fieldsets(self, request, obj=None):
fieldsets = super(MyAdmin, self).get_fieldsets(request, obj)
newfieldsets = list(fieldsets)
fields = ['foo', 'bar', 'baz']
newfieldsets.append(['Dynamic Fields', { 'fields': fields }])
return newfieldsets
This works for adding dynamic fields in Django 1.9.3, using just a ModelAdmin class (no ModelForm) and by overriding get_fields. I don't know yet how robust it is:
class MyModelAdmin(admin.ModelAdmin):
fields = [('title','status', ), 'description', 'contact_person',]
exclude = ['material']
def get_fields(self, request, obj=None):
gf = super(MyModelAdmin, self).get_fields(request, obj)
new_dynamic_fields = [
('test1', forms.CharField()),
('test2', forms.ModelMultipleChoiceField(MyModel.objects.all(), widget=forms.CheckboxSelectMultiple)),
]
#without updating get_fields, the admin form will display w/o any new fields
#without updating base_fields or declared_fields, django will throw an error: django.core.exceptions.FieldError: Unknown field(s) (test) specified for MyModel. Check fields/fieldsets/exclude attributes of class MyModelAdmin.
for f in new_dynamic_fields:
#`gf.append(f[0])` results in multiple instances of the new fields
gf = gf + [f[0]]
#updating base_fields seems to have the same effect
self.form.declared_fields.update({f[0]:f[1]})
return gf
Maybe I am a bit late... However, I am using Django 3.0 and also wanted to dynamically ad some custom fields to the form, depending on the request.
I end up with a solution similar to the one described by #tehfink combined with #little_birdie.
However, just updating self.form.declared_fields as suggested didn't help. The result of this procedure is, that the list of custom fields defined in self.form.declared_fields always grows from request to request.
I solved this by initialising this dictionary first:
class ModelAdminGetCustomFieldsMixin(object):
def get_fields(self, request, obj=None):
fields = super().get_fields(request, obj=None)
self.form.declared_fields = {}
if obj:
for custom_attribute in custom_attribute_list:
self.form.declared_fields.update({custom_attribute.name: custom_attribute.field})
return fields
where custom_attribute.field is a form field instance.
Additionally, it was required to define a ModelForm, wherein during initialisation the custom fields have been added dynamically as well:
class SomeModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for custom_attribute in custom_attribute_list:
self.fields[custom_attribute.name] = custom_attribute.field
and use this ModelForm in the ModelAdmin.
Afterwards, the newly defined attributes can be used in, e.g., a fieldset.
While Jacob's post might work all right for regular ModelForms (even though it's more than a year and a half old), the admin is a somewhat different matter.
All the declarative way of defining models, forms ModelAdmins and whatnot makes heavy use of metaclasses and class introspection. Same with the admin – when you tell a ModelAdmin to use a specific form istead of creating a default one, it introspects the class. It gets the list of fields and other stuff from the class itself without instantiating it.
Your custom class, however, does not define the extra form field at class level, instead it dynamically adds one after it has been instantiated – that's too late for the ModelAdmin to recognize this change.
One way to go about your problem might be to subclass ModelAdmin and override its get_fieldsets method to actually instantiate the ModelForm class and get the list of fields from the instance instead of the class. You'll have to keep in mind, though, that this might be somewhat slower than the default implementation.
You can create dynamic fields and fieldset using the form meta class. Sample code is given below. Add the loop logic as per you requirements.
class CustomAdminFormMetaClass(ModelFormMetaclass):
"""
Metaclass for custom admin form with dynamic field
"""
def __new__(cls, name, bases, attrs):
for field in get_dynamic_fields: #add logic to get the fields
attrs[field] = forms.CharField(max_length=30) #add logic to the form field
return super(CustomAdminFormMetaClass, cls).__new__(cls, name, bases, attrs)
class CustomAdminForm(six.with_metaclass(CustomAdminFormMetaClass, forms.ModelForm)):
"""
Custom admin form
"""
class Meta:
model = ModelName
fields = "__all__"
class CustomAdmin(admin.ModelAdmin):
"""
Custom admin
"""
fieldsets = None
form = CustomAdminForm
def get_fieldsets(self, request, obj=None):
"""
Different fieldset for the admin form
"""
self.fieldsets = self.dynamic_fieldset(). #add logic to add the dynamic fieldset with fields
return super(CustomAdmin, self).get_fieldsets(request, obj)
def dynamic_fieldset(self):
"""
get the dynamic field sets
"""
fieldsets = []
for group in get_field_set_groups: #logic to get the field set group
fields = []
for field in get_group_fields: #logic to get the group fields
fields.append(field)
fieldset_values = {"fields": tuple(fields), "classes": ['collapse']}
fieldsets.append((group, fieldset_values))
fieldsets = tuple(fieldsets)
return fieldsets
Stephan's answer is elegant, but when I used in in dj1.6 it required the field to be a tuple.
The complete solution looked like this:
class ProductForm(ModelForm):
foo = CharField(label='foo')
class ProductAdmin(admin.ModelAdmin):
form = ProductForm
def get_fieldsets(self, request, obj=None):
fieldsets = super(ProductAdmin, self).get_fieldsets(request, obj)
fieldsets[0][1]['fields'] += ('foo', )
return fieldsets
not sure why that's not working, but could a possible workaround be to define the field statically (on the form) and then override it in the __init__?
I for a long time could not solve a problem with dynamic addition of fields.
The solution "little_birdie" really works. Thank you Birdie))
The only nuance is:
"Self.declared_fieldsets" should be replaced with "self.fieldsets".
#kwargs['fields'] = flatten_fieldsets(self.declared_fieldsets)
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
I used version 1.10. Perhaps something has changed.
If someone finds an even simpler and elegant solution, show here.
Thanks to all )))