I would like to provide different widgets to input form fields for the same type of model field in a Django admin inline.
I have implemented a version of the Entity-Attribute-Value paradigm in my shop application (I tried eav-django and it wasn't flexible enough). In my model it is Product-Parameter-Value (see Edit below).
Everything works as I want except that when including an admin inline for the Parameter-Value pair, the same input formfield is used for every value. I understand that this is the default Django admin behaviour because it uses the same formset for each Inline row.
I have a callback on my Parameter that I would like to use (get_value_formfield). I currently have:
class SpecificationValueAdminInline(admin.TabularInline):
model = SpecificationValue
fields = ('parameter', 'value')
readonly_fields = ('parameter',)
max_num = 0
def get_formset(self, request, instance, **kwargs):
"""Take a copy of the instance"""
self.parent_instance = instance
return super().get_formset(request, instance, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
"""Override admin function for requesting the formfield"""
if self.parent_instance and db_field.name == 'value':
# Notice first() on the end -->
sv_instance = SpecificationValue.objects.filter(
product=self.parent_instance).first()
formfield = sv_instance.parameter.get_value_formfield()
else:
formfield = super().formfield_for_dbfield(db_field, **kwargs)
return formfield
formfield_for_dbfield is only called once for each admin page.
How would I override the default behaviour so that formfield_for_dbfield is called once for each SpecificationValue instance, preferably passing the instance in each time?
Edit:
Here is the model layout:
class Product(Model):
specification = ManyToManyField('SpecificationParameter',
through='SpecificationValue')
class SpecificationParameter(Model):
"""Other normal model fields here"""
type = models.PositiveSmallIntegerField(choices=TUPLE)
def get_value_formfield(self):
"""
Return the type of form field for parameter instance
with the correct widget for the value
"""
class SpecificationValue(Model):
product = ForeignKey(Product)
parameter = ForeignKey(SpecificationParameter)
# To store and retrieve all types of value, overrides CharField
value = CustomValueField()
The way I eventually solved this is using the form = attribute of the Admin Inline. This skips the form generation code of the ModelAdmin:
class SpecificationValueForm(ModelForm):
class Meta:
model = SpecificationValue
def __init__(self, instance=None, **kwargs):
super().__init__(instance=instance, **kwargs)
if instance:
self.fields['value'] = instance.parameter.get_value_formfield()
else:
self.fields['value'].disabled = True
class SpecificationValueAdminInline(admin.TabularInline):
form = SpecificationValueForm
Using standard forms like this, widgets with choices (e.g. RadioSelect and CheckboxSelectMultiple) have list bullets next to them in the admin interface because the <ul> doesn't have the radiolist class. You can almost fix the RadioSelect by using AdminRadioSelect(attrs={'class': 'radiolist'}) but there isn't an admin version of the CheckboxSelectMultiple so I preferred consistency. Also there is an aligned class missing from the <fieldset> wrapper element.
Looks like I'll have to live with that!
Is it possible to build a custom model field/widget combination which displays a value but never writes anything back to the database? I would use this widget exclusively in the admin's forms.
I wrote my own field, which overwrites the formfield() method to declare its own widget class. It displays just fine, but as soon as the 'Save' button is clicked in the admin, I'm getting a validation error:
This field is required.
That makes sense, considering that my widget didn't render out a form field. However, what I'd like to do is basically remove this field from the update process: whenever used in the admin, it just shouldn't be mentioned in the SQL UPDATE at all.
Is that possible?
Here's a sketch of the code I have so far:
class MyWidget(Widget):
def render(self, name, value, attrs=None):
if value is None:
value = ""
else:
# pretty print the contents of value here
return '<table>' + ''.join(rows) + '</table>'
class MyField(JSONField):
def __init__(self, *args, **kwargs):
kwargs['null'] = False
kwargs['default'] = list
super(MyField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {
'form_class': JSONFormField,
'widget': MyWidget,
}
defaults.update(**kwargs)
return super(MyField, self).formfield(**defaults)
UPDATE 1: The use case is that the field represents an audit log. Internally, it will be written to regularly. The admin however never needs to write to it, it only has to render it out in a very readable format.
I'm not using any other ModelForms in the application, so the admin is the only form-user. I don't want to implement the behavior on the admin classes themselves, because this field will be reused across various models and is always supposed to behave the same way.
There are multiple ways to create a read-only field in the admin pages. Your requirements on the database storage are a bit fuzzy so I go through the options.
You have to register an AdminModel first in admin.py:
from django.contrib import admin
from yourapp.models import YourModel
class YourAdmin(admin.ModelAdmin):
pass
admin.site.register(YourModel, YourAdmin)
Now you can add different behavior to it. For example you can add the list of fields shown in the edit/add page:
class YourAdmin(admin.ModelAdmin):
fields = ['field1', 'field2']
This can be names of the model fields, model properties or model methods. Methods are displayed read-only.
If you want to have one field read-only explicitly add this:
class YourAdmin(admin.ModelAdmin):
fields = ['field1', 'field2']
readonly_fields = ['field2']
Then you have the option to overwrite the display of the field completely by adding a method with the same name. You will not even need a model field/method with that name, then:
class YourAdmin(admin.ModelAdmin):
fields = ['field1', 'field2']
readonly_fields = ['field2']
def field2(self, obj):
return '*** CLASSIFIED *** {}'.format(obj.field2)
With django.utils.safestring.mark_safe you can return HTML code as well.
All other options of the Admin are available, except the widget configuration as it applies to the writable fields only.
I might be a little confused as to what you want but you might want to look into model properties. Here is an example for my current project.
Code inside your model:
class Textbook(models.Model):
#other fields
#property
def NumWishes(self):
return self.wishlist_set.count()
Then you can just display it on the admin page.
class Textbook_table(admin.ModelAdmin):
fields = ["""attributes that are saved in the model"""]
list_display = ("""attributes that are saved in the model""", 'NumWishes'')
So now I can display NumWishes in the admin page but it doesn't need to be created with the model.
Hello in the class admin modify the permission method
#admin.register(my_model)
class My_modelAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
I got a model Layout in my Django app with the following fields:
meta_layout - ForeignKey on model MetaLayout
name - CharField
edited - DateTimeField
is_active - BooleanField
And I have two views using this model - one called NewLayout and other EditLayout each subclassing standard CreateView and UpdateView accordingly. In EditLayout view I want to use some special form that looks the same as form used in NewLayout (which is simply plain ModelForm for this model) but has meta_layout select field displayed with attribute disabled="disabled" (e.d. user can choose meta_layout for each Layout only once - while creating it). Ok, I can create custom ModelForm where widget for meta_layout field has the desired attribute, but the problem is actually that when such attribute set on form field it will not send any values with request - so my validation fails trying to check value for this field and select element does not support "readonly" attribute which will would be just fine here.
I found some really ugly hack to workaround this:
#Here is my Form:
class LayoutEditForm(forms.ModelForm):
meta_layout = forms.ModelChoiceField(
queryset=MetaLayout.objects.all(),
widget=forms.Select(attrs=dict(disabled='disabled')),
empty_label=None,
required=False) # if required=True validation will fail
# because value is not supplied in POST
class Meta:
fields = ('meta_layout', 'name', 'is_active')
model = Layout
class EditLayout(UpdateView):
...
# And one modified method from my View-class
def get_form_kwargs(self):
kwargs = super(EditLayout, self).get_form_kwargs()
# actually POST parameters
if kwargs.has_key('data'):
# can't change QueryDict itself - it's immutable
data = dict(self.request.POST.items())
# emulate POST params from ModelChoiceField
data['meta_layout'] = u'%d' % self.object.meta_layout.id
kwargs['data'] = data
return kwargs
But I believe that it's non-Django, non-Pythonic and not a good-programming-style-at-all of doing such simple thing. Can you suggest any better solution?
Edit:
Oh, I found much less ugly solution: added this in my form class:
def clean_meta_layout(self):
return self.instance.meta_layout
But I still open for suggestions) - may I missed something?
I'm trying to pass on additional information to fields of a Django form to be displayed in a template. I tried to override the constructor and add another property to the field like this:
self.fields['field_name'].foo = 'bar'
but in the template this:
{{ form.field_name.foo }}
didn't print anything. Does anyone know how to add additional information to a field without rewriting/inheriting the forms field classes?
According to django.forms.forms, the __getitem__() method of a Form creates something called a BoundField out of the Field before returning it, thus stripping it of whatever changes you made. If you really want to insert more functionality into that, override that method to do stuff to the bound field before returning it:
class MyForm(forms.Form):
def __getitem__(self, name):
boundfield = super(forms.Form,self).__getitem__(name)
boundfield.foo = "bar"
return boundfield
Then, "bar" will appear for all fields in that form. You can also make a function and call that instead, to make it more than just a hard-coded string.
While it's more standard to add more fields, or to add properties to the form itself, if you have a whole new class of info that every field needs to contain, this may do it for you.
Another way to get the same thing is to edit an attribute of the field, then access it via the BoundField's "field" attribute:
class MyForm(forms.Form):
def __init__(self, *args, **kwargs)
super(forms.Form, self).__init__(*args, **kwargs)
self.fields['field_name'].foo = "bar"
Then, to access foo in a template:
{{ form.field_name.field.foo }}
Whenever I'm editing object A with a foreign key to object B, a plus option "add another" is available next to the choices of object B. How do I remove that option?
I configured a user without rights to add object B. The plus sign is still available, but when I click on it, it says "Permission denied". It's ugly.
I'm using Django 1.0.2
The following answer was my original answer but it is wrong and does not answer OP's question:
Simpler solution, no CSS hack and no editing Django codebase:
Add this to your Inline class:
max_num=0
(this is only applicable to inline forms, not foreign key fields as OP asked)
The above answer is only useful to hide the "add related" button for inline forms, and not foreign keys as requested.
When I wrote the answer, IIRC the accepted answer hid both, which is why I got confused.
The following seems to provide a solution (though hiding using CSS seems the most feasible thing to do, especially if the "add another" buttons of FKs are in inline forms):
Django 1.7 removing Add button from inline form
Though most of the solutions mentioned here work, there is another cleaner way of doing it. Probably it was introduced in a later version of Django, after the other solutions were presented. (I'm presently using Django 1.7)
To remove the "Add another" option,
class ... #(Your inline class)
def has_add_permission(self, request):
return False
Similarly if you want to disable "Delete?" option, add the following method in Inline class.
def has_delete_permission(self, request, obj=None):
return False
N.B. Works for DJango 1.5.2 and possibly older. The can_add_related property appeared around 2 years ago.
The best way I've found is to override your ModelAdmin's get_form function. In my case I wanted to force the author of a post to be the currently logged in user. Code below with copious comments. The really important bit is the setting of widget.can_add_related:
def get_form(self,request, obj=None, **kwargs):
# get base form object
form = super(BlogPostAdmin,self).get_form(request, obj, **kwargs)
# get the foreign key field I want to restrict
author = form.base_fields["author"]
# remove the green + by setting can_add_related to False on the widget
author.widget.can_add_related = False
# restrict queryset for field to just the current user
author.queryset = User.objects.filter(pk=request.user.pk)
# set the initial value of the field to current user. Redundant as there will
# only be one option anyway.
author.initial = request.user.pk
# set the field's empty_label to None to remove the "------" null
# field from the select.
author.empty_label = None
# return our now modified form.
return form
The interesting part of making the changes here in get_form is that author.widget is an instance of django.contrib.admin.widgets.RelatedFieldWidgetWrapper where as if you try and make changes in one of the formfield_for_xxxxx functions, the widget is an instance of the actual form widget, in this typical ForeignKey case it's a django.forms.widgets.Select.
I use the following approaches for Form and InlineForm
Django 2.0, Python 3+
Form
class MyModelAdmin(admin.ModelAdmin):
#...
def get_form(self,request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
user = form.base_fields["user"]
user.widget.can_add_related = False
user.widget.can_delete_related = False
user.widget.can_change_related = False
return form
Inline Form
class MyModelInline(admin.TabularInline):
#...
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
user = formset.form.base_fields['user']
user.widget.can_add_related = False
user.widget.can_delete_related = False
user.widget.can_change_related = False
return formset
The answer by #Slipstream shows how to implement the solution, viz. by overriding the attributes for the formfield's widget, but, in my opinion, get_form is not the most logical place to do this.
The answer by #cethegeek shows where to implement the solution, viz. in an extension of formfield_for_dbfield, but does not provide an explicit example.
Why use formfield_for_dbfield? Its docstring suggests that it is the designated hook for messing with form fields:
Hook for specifying the form Field instance for a given database Field instance.
It also allows for (slightly) cleaner and clearer code, and, as a bonus, we can easily set additional form Field attributes, such as initial value and/or disabled (example here), by adding them to the kwargs (before calling super).
So, combining the two answers (assuming the OP's models are ModelA and ModelB, and the ForeignKey model field is named b):
class ModelAAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, request, **kwargs):
# optionally set Field attributes here, by adding them to kwargs
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name == 'b':
formfield.widget.can_add_related = False
formfield.widget.can_change_related = False
formfield.widget.can_delete_related = False
return formfield
# Don't forget to register...
admin.site.register(ModelA, ModelAAdmin)
NOTE: If the ForeignKey model field has on_delete=models.CASCADE, the can_delete_related attribute is False by default, as can be seen in the source for RelatedFieldWidgetWrapper.
Look at django.contrib.admin.options.py and check out the BaseModelAdmin class, formfield_for_dbfield method.
You will see this:
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
I think your best bet is create subclass of ModelAdmin (which in turn is a subclass of BaseModelAdmin), base your model on that new class, override formfield_fo_dbfield and make it so that it won't/or will conditionally wrap the widget in RelatedFieldWidgetWrapper.
One could argue that if you have a user that doesn't have rights to adding related objects, the RelatedFieldWidgetWrapper should not display the add link? Maybe this is something that is deserving of mention in Django trac?
DEPRECATED ANSWER
Django has since made this possible.
Have you considered instead, using CSS to simply not show the button? Maybe that's a little too hacky.
This is untested, but I'm thinking...
no-addanother-button.css
#_addanother { display: none }
admin.py
class YourAdmin(admin.ModelAdmin):
# ...
class Media:
# edit this path to wherever
css = { 'all' : ('css/no-addanother-button.css',) }
Django Doc for doing this -- Media as a static definition
Note/Edit: The documentation says the files will be prepended with the MEDIA_URL but in my experimentation it isn't. Your mileage may vary.
If you find this is the case for you, there's a quick fix for this...
class YourAdmin(admin.ModelAdmin):
# ...
class Media:
from django.conf import settings
media_url = getattr(settings, 'MEDIA_URL', '/media/')
# edit this path to wherever
css = { 'all' : (media_url+'css/no-addanother-button.css',) }
I'm using Django 2.x and I think I found best solution, at least for my case.
The HTML file to the "Save and Add Another" button is on your_python_installation\Lib\site-packages\django\contrib\admin\templates\admin\subtmit_line.html.
Copy that html file and paste to your project like so your_project\templates\admin\submit_line.html.
Open it and comment/delete the button code as desired:
{#{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}#}
I know this problem is already answered. But maybe someone in the future have a similar case with me.
Based on cethegeek answer I made this:
class SomeAdmin(admin.ModelAdmin):
form = SomeForm
def formfield_for_dbfield(self, db_field, **kwargs):
formfield = super(SomeAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'some_m2m_field':
request = kwargs.pop("request", None)
formfield = self.formfield_for_manytomany(db_field, request, **kwargs) # for foreignkey: .formfield_for_foreignkey
wrapper_kwargs = {'can_add_related': False, 'can_change_related': False, 'can_delete_related': False}
formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(
formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
)
return formfield
The way i fixed a similar situation based on django docs
https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.InlineModelAdmin.extra
The outcome of the solution is that it lets you add an inline just for that instance. Or in different words: add an inline and just one; no other buttons.
#models.py
class Model_A(models.Model):
...
class Model_B(models.Model):
...
relevant_field = models.ForeignKey(Model_A, related_name='Model_B_relevant_field')
# forms.py or someotherfile.py
from django.contrib.admin import StackedInline, TabularInline
class Model_B_Inline(StackedInline):
verbose_name = 'Some Name'
...
def get_extra(self, request, obj=None, *args, **kwargs):
the_extra = super().get_extra(request, obj=obj, *args, **kwargs)
self.extra = 1
if obj:
the_counter = obj.Model_B_relevant_field.count()
else:
the_counter = -1
self.max_num = the_counter + 1
return the_extra
As it's been pointed out in comments:
max_num = 0
It has also been confirmed here:
https://code.djangoproject.com/ticket/13424#comment:1
PS: This also works for inlines.
django.contrib.admin.widgets.py
(Django Install Dir)/django/contrib/admin/widgets.py: Comment everything between Line 239 & Line 244:
if rel_to in self.admin_site._registry: # If the related object has an admin interface:
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \
(related_url, name))
output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Add Another')))