Remove "add another" in Django admin screen - python

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')))

Related

Django FilePathField filter used entries

I have the following Model where the FilePathField should be unique:
class Gallery(models.Model):
template = models.FilePathField(path=".../templates/galleries/", unique=True)
In the admin, I would like the dropdown list to only show me those entries that have not been used, yet, in order to make the selection among available answers more easy.
After all, any already used option in the resulting dropdown list will give me an error anyway and does not need to be shown to me in the admin. Unfortunately I am having problems wrapping my head around this.
Can anyone tell me where I could insert something similar to the following:
used = [gallery.template for gallery in Gallery.objects.all()]
return [file for file in files if file not in used]
...or might I have overseen an option somewhere in Django that could already give me the desired result? Any help would be appreciated.
So, after a lot of digging, I managed to come up with a solution myself. Ill post it here as an answer if anyone seeks a similar solution:
Extend a ModelAdmin for your Model and implement a new get_form() method that takes the choices of your named FilePathField and filter this list to your liking.
Ill give an example for the Gallery Model above:
class GalleryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
used = [gallery.template for gallery in Gallery.objects.all()]
form = super(GalleryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['template'].choices = [choice for choice in form.base_fields['template'].choices if choice[0] not in used]
return form
EDIT: I noticed this prevents you from changing an entry, as the option originally set will now be removed. I managed to get this to work with this small tweak:
class GalleryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj: # we are changing an entry
used = [gallery.template for gallery in Gallery.objects.all() if gallery.template != obj.template]
else: # we are adding a new entry
used = [gallery.template for gallery in Gallery.objects.all()]
form = super(GalleryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['template'].choices = [choice for choice in form.base_fields['template'].choices if choice[0] not in used]
return form
Hope this may help anyone in the future!

Disable choice list in Django admin, only for editing

I want to disable some fields when I am editing an object. I have managed to do this for text fields, but it's been impossible for a dropdown list (choice list).
I am doing this action in the constructor of the form.
class OrderModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['description'].widget.attrs['readonly'] = True
self.fields['city_code'].widget.attrs['disabled'] = True
Notice how I made it for both with different keywords, but I can't do it for my customer_id field.
Setting the attribute to disabled or readonly only affects the way the widgets are displayed. It doesn't actually stop somebody submitting a post request that changes those fields.
It might be a better approach to override get_readonly_fields for your model.
class OrderModelAdmin(admin.Model
def get_readonly_fields(self, request, obj=None):
if self.obj.pk:
return ['description', 'city_code', 'customer']
else:
return []
The answer of #Alasdair is better than this one (because this one doesn't prevent a submission). But I post it, just in case someone wants the equivalent to 'readonly' for ModelChoiceField.
self.fields['customer_id'].widget.widget.attrs['disabled'] = 'disabled'
Notice, that for a ChoiceField is enought something like this:
self.fields['city_code'].widget.attrs['disabled'] = True

Creating a display-only (non-editable) Django admin field

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

Django MultiSelectField: Make some choices readonly when editing

I'm using Django MultiSelectField to store a project's category/categories.
My requirement is as follows: A project can have one or more categories. When creating a project initially, the user should be able to select any of the categories listed in the select field. If a user selects 'Not Applicable', then the other categories become disabled.
When editing a project, user should not be able to unselect the originally selected categorie(s), but he can add/select other categories in addition to the original ones. The originally selected categories should be readonly when editing.
My model looks like this:
from multiselectfield import MultiSelectField
(...)
class Project(models.Model):
CAT_0 = 0
CAT_1 = 1
CAT_2 = 2
CAT_3 = 3
PROJECT_CATEGORIES = (
(CAT_0, _('Not Applicable')),
(CAT_1, _('My Category 1')),
(CAT_2, _('My Category 2')),
(CAT_3, _('My Category 3')),
)
(...)
project_categories = MultiSelectField(choices=PROJECT_CATEGORIES, max_choices=3, default=CAT_0)
And my forms.py
class ProjectForm(ModelForm):
class Meta:
model = Project
(...)
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
(...)
if self.instance.pk:
for choice in self.fields['project_categories'].choices:
if str(choice[0]) in self.instance.project_categories:
#TODO: Make this choice to be readonly i.e. user should not be able to uncheck it
else:
#TODO: Leave the choice as it is i.e. user can select/check it
# This is what I had earlier, but it makes all the choices to be readonly. Not what I want
self.fields['strategic_objectives'].widget.attrs['readonly'] = True
self.fields['strategic_objectives'].widget.attrs['disabled'] = True
How do I make sure that the original categories are shown as readonly?
Is this the way to go about it or JS will be a better option?
Is kinda useless doing that in backend before rendering, because an user can modifies your html code and enable it again.
So, disable them using JS and then in your Backend after user submit the form, you need to check it.
No metters what, have front-end validations is up to you, but you always need to have them in your backend as well.
I had the same problem as you and solved it with the help of this blogpost.
Basically it is necessary to modify the widget which is used to display the choices to be able to mark some choices as disabled.
So I created a widgets.py with the following content:
from django.forms import CheckboxSelectMultiple
class CheckboxSelectMultipleWithDisabled(CheckboxSelectMultiple):
def __init__(self, *args, **kwargs):
self.disabled = kwargs.pop("disabled", set())
super().__init__(*args, **kwargs)
def create_option(self, *args, **kwargs):
options_dict = super().create_option(*args, **kwargs)
if options_dict["value"] in self.disabled:
options_dict["attrs"]["disabled"] = True
return options_dict
Then in the constructor of the form class (ProjectForm) I exchanged the widget of the respective field (project_categories) and used the added disabled keyword argument introduced above:
def __init__(self, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
# fill a local 'disabled' set/list/tuple/... variable using your logic
self.fields['project_categories'].widget = CheckboxSelectMultipleWithDisabled(
choices=self.fields['project_categories'].choices,
disabled=disabled
)
Also do not forget to import the new widget to use it: from .widgets import CheckboxSelectMultipleWithDisabled.
What levi posted is still true, though: you can remove the disabled tag in the html code in your browser and be able to modify the choice again.
So without any extra validation you have to trust your users.
A similar solution is presented in the answer to this question.

Django models: Only permit one entry in a model?

I want to make some of my Django global settings configurable through the admin interface.
To that end, I've decided to set them as database fields, rather than in settings.py.
These are the settings I care about:
class ManagementEmail(models.Model):
librarian_email = models.EmailField()
intro_text = models.CharField(max_length=1000)
signoff_text = models.CharField(max_length=1000)
These are one-off global settings, so I only ever want there to be a single librarian_email, intro_text etc floating around the system.
Is there a way I can prevent admin users from adding new records here, without preventing them from editing the existing record?
I guess I can do this by writing a custom admin template for this model, but I'd like to know if there's a neater way to configure this.
Could I use something other than class, for example?
Thanks!
Please see this question on "keep[ing] settings in database", where the answer seems to be django-dbsettings
Update
Just thought of another option: you can create the following model:
from django.contrib.sites.models import Site
class ManagementEmail(models.Model):
site = models.OneToOneField(Site)
librarian_email = models.EmailField()
intro_text = models.CharField(max_length=1000)
signoff_text = models.CharField(max_length=1000)
Because of the OneToOneField field, you can only have one ManagementEmail record per site. Then, just make sure you're using sites and then you can pull the settings thusly:
from django.contrib.sites.models import Site
managementemail = Site.objects.get_current().managementemail
Note that what everyone else is telling you is true; if your goal is to store settings, adding them one by one as fields to a model is not the best implementation. Adding settings over time is going to be a headache: you have to add the field to your model, update the database structure, and modify the code that is calling that setting.
That's why I'd recommend using the django app I mentioned above, since it does exactly what you want -- provide for user-editable settings -- without making you do any extra, unnecessary work.
I think the easiest way you can do this is using has_add_permissions function of the ModelAdmin:
class ContactUsAdmin(admin.ModelAdmin):
form = ContactUsForm
def has_add_permission(self, request):
return False if self.model.objects.count() > 0 else super().has_add_permission(request)
You can set the above to be any number you like, see the django docs.
If you need more granularity than that, and make the class a singleton at the model level, see django-solo. There are many singleton implementations also that I came across.
For StackedInline, you can use max_num = 1.
Try django-constance.
Here are some useful links:
https://github.com/jezdez/django-constance
http://django-constance.readthedocs.org/en/latest/
I'd take a page out of wordpress and create a Model that support settings.
class Settings(models.Model):
option_name = models.CharField(max_length=1000)
option_value = models.CharField(max_length=25000)
option_meta = models.CharField(max_length=1000)
Then you can just pickle (serialize) objects into the fields and you'll be solid.
Build a little api, and you can be as crafty as wordpress and call. AdminOptions.get_option(opt_name)
Then you can just load the custom settings into the runtime, keeping the settings.py module separate, but equal. A good place to write this would be in an __init__.py file.
Just set up an GlobalSettings app or something with a Key and Value field.
You could easily prevent admin users from changing values by not giving them permission to edit the GlobalSettings app.
class GlobalSettingsManager(models.Manager):
def get_setting(self, key):
try:
setting = GlobalSettings.objects.get(key=key)
except:
raise MyExceptionOrWhatever
return setting
class GlobalSettings(models.Model):
key = models.CharField(unique=True, max_length=255)
value = models.CharField(max_length=255)
objects = GlobalSettingsManager()
>>> APP_SETTING = GlobalSettings.objects.get_setting('APP_SETTING')
There are apps for this but I prefer looking at them and writing my own.
You can prevent users from adding/deleting an object by overriding this method on your admin class:
ModelAdmin.has_add_permission(self, request)
ModelAdmin.has_delete_permission(self, request, obj=None)
Modification of #radtek answer to prevent deleting if only one entry is left
class SendgridEmailQuotaAdmin(admin.ModelAdmin):
list_display = ('quota','used')
def has_add_permission(self, request):
return False if self.model.objects.count() > 0 else True
def has_delete_permission(self, request, obj=None):
return False if self.model.objects.count() <= 1 else True
def get_actions(self, request):
actions = super(SendgridEmailQuotaAdmin, self).get_actions(request)
if(self.model.objects.count() <= 1):
del actions['delete_selected']
return actions
I had basically the same problem as the original poster describes, and it's easily fixed by overriding modelAdmin classes. Something similar to this in an admin.py file easily prevents adding a new object but allows editing the current one:
class TitleAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=TestModel.title):
return False
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=TestModel.title):
return True
This doesn't prevent a user from posting a form that edits data, but keeps things from happening in the Admin site. Depending on whether or not you feel it's necessary for your needs you can enable deletion and addition of a record with a minimum of coding.

Categories