18

I'm having problems achieving a (probably) rather simple task. I have fully modifiable models (Prodotto, Comune) which are shown as "addable" fields, as shown in picture below. What I'd rather not see is the + (add) button for such fields, hence to remove their "addable" propriety in this form. I've tried setting has_add_permission=False within the two models, but it would make it impossibile to add new objects to such models completely, not only in this form.

How can I do that?

EDIT: To clarify my need, I'd like NOT to have the "+"s next to the fields of the FK models, but I do still want to be able to add whole new inlines. To be as clear as possible, as I wrote in a comment, considering a scenario such as this: https://code.djangoproject.com/attachment/ticket/20367/django_custom_user_admin_form.png I just need to have the "+"s next to Groups and Country removed.

EXISTING CODE:

models.py (of the specific application involved):

from django.db import models

from smart_selects.db_fields import ChainedForeignKey

from apps.comune.models import Comune, Cap


class Prodotto(models.Model):
    SETTORE_CHOICES = (
        ('CAL', 'Accessori calzature'),
        ('ALI', 'Alimentari'),
        ('ARA', 'Arredamenti e accessori'),
        ('AEM', 'Auto e moto'),
        ('CAL', 'Calzature'),
        ('CEG', 'Cartaria e grafica'),
        ('CEP', 'Concerie e pelletterie'),
        ('EDI', 'Edilizia'),
        ('INV', 'Industrie varie'),
        ('IST', 'Istruzione'),
        ('MDC', 'Materiali da costruzione'),
        ('MMC', 'Metalmeccanica'),
        ('SEI', 'Serramenti e infissi'),
        ('STM', 'Strumenti musicali'),
        ('TEI', 'Terziario innovativo'),
        ('TAB', 'Tessile abbigliamento'),
        ('TCP', 'Trasporto cose e persone'),
        ('VAR', 'Vari'),
    )
    nome = models.CharField(max_length=100)
    settore = models.CharField(max_length=40, choices=SETTORE_CHOICES)

    class Meta:
        verbose_name_plural = "prodotti"
        verbose_name = "prodotto"
        ordering = ['nome']

    def __unicode__(self):
        return self.nome.capitalize()


class Cliente(models.Model):
    TIPOLOGIA_CHOICES = (
        ('AR', 'Artigiano'),
        ('CO', 'Commerciante'),
        ('GI', 'Grande impresa'),
        ('PI', 'Piccola impresa'),
    )
    FORMA_SOCIETARIA_CHOICES = (
        ('SNC', 'S.n.c.'),
        ('SRL', 'S.r.l.'),
        ('SPA', 'S.p.A.'),
        ('SAS', 'S.a.s.'),
        ('COOP', 'Coop.A.r.l.'),
        ('DI', 'D.I.'),
        ('SCARL', 'S.c.a.r.l.'),
        ('SCPA', 'S.c.p.a.'),
    )
    SETTORE_CHOICES = (
        ('CAL', 'Accessori calzature'),
        ('ALI', 'Alimentari'),
        ('ARA', 'Arredamenti e accessori'),
        ('AEM', 'Auto e moto'),
        ('CAL', 'Calzature'),
        ('CEG', 'Cartaria e grafica'),
        ('CEP', 'Concerie e pelletterie'),
        ('EDI', 'Edilizia'),
        ('INV', 'Industrie varie'),
        ('IST', 'Istruzione'),
        ('MDC', 'Materiali da costruzione'),
        ('MMC', 'Metalmeccanica'),
        ('SEI', 'Serramenti e infissi'),
        ('STM', 'Strumenti musicali'),
        ('TEI', 'Terziario innovativo'),
        ('TAB', 'Tessile abbigliamento'),
        ('TCP', 'Trasporto cose e persone'),
        ('VAR', 'Vari'),
    )
    ragione_sociale = models.CharField(max_length=200)
    forma_societaria = models.CharField(
        max_length=5, choices=FORMA_SOCIETARIA_CHOICES)
    titolare = models.CharField(max_length=100, blank=True)
    partita_iva = models.CharField(
        max_length=11, verbose_name='Partita IVA', unique=True)
    tipologia = models.CharField(max_length=2, choices=TIPOLOGIA_CHOICES)
    settore = models.CharField(max_length=40, choices=SETTORE_CHOICES)
    prodotto = models.ManyToManyField(Prodotto, blank=True)

    class Meta:
        verbose_name_plural = "clienti"
        verbose_name = "cliente"

    def __unicode__(self):
        return self.ragione_sociale.capitalize()


class Sede(models.Model):
    nome = models.CharField(max_length=100)
    indirizzo = models.CharField(max_length=200, blank=True)
    cliente = models.ForeignKey(Cliente)
    comune = models.ForeignKey(Comune)
    cap = ChainedForeignKey(
        Cap,
        chained_field="comune",
        chained_model_field="comune",
        show_all=False,
        auto_choose=True,
    )

    class Meta:
        verbose_name_plural = "sedi"
        verbose_name = "sede"
        ordering = ['nome']

    def __unicode__(self):
        return self.nome.capitalize() + ", " + self.indirizzo

admin.py (of the specific application involved):

from django.contrib import admin

from .models import Cliente, Prodotto, Sede
from apps.recapito.models import RecapitoCliente


class SedeInline(admin.TabularInline):
    model = Sede
    extra = 1

    def provincia(self, obj):
        return obj.comune.provincia

    readonly_fields = ['provincia', ]


class RecapitoInline(admin.TabularInline):
    model = RecapitoCliente
    extra = 1
    list_fields = ['cliente', 'tipo', 'recapito', ]


@admin.register(Cliente)
class ClienteAdmin(admin.ModelAdmin):
    list_display = [
        'ragione_sociale', 'forma_societaria', 'titolare', 'partita_iva', ]
    list_filter = ['forma_societaria', ]
    search_fields = ['ragione_sociale', ]
    inlines = [RecapitoInline, SedeInline]


admin.site.register(Prodotto)

The admin interface of this app produces this:

Admin interface

Shortcut links 1 and 2 are the ones I need removed, being referred to columns (FKs) inside my inline classes. Shortcut links 3 and 4 are to be kept, since they refers to the inlines themselves.

Andy Baker
  • 21,158
  • 12
  • 58
  • 71
Seether
  • 1,524
  • 1
  • 14
  • 28

4 Answers4

26

To remove the "Add another" option, please add the below method in admin inline class.

def has_add_permission(self, request):
    return False

Similarly if you want to disable "Delete?" option, add the following method in admin inline class.

def has_delete_permission(self, request, obj=None):
    return False
Sandy
  • 827
  • 9
  • 6
  • To the top! There's no need messing with widgets ;) – Andreas Dolk Aug 11 '16 at 12:24
  • 8
    This is not what is wanted - this answer removes the option to add another instance of the inline model, not the option to add/edit the inline's foreign key fields (as described in the EDIT to the question). – jenniwren Aug 30 '16 at 22:09
  • Note: this will remove the "add another" LINK, but nonetheless execute the related SQL query and render the widget in the dom containing the results of this request. – ppython Mar 14 '17 at 17:12
21

I think this is a less hacky solution than the one you ended up with. It worked for me, anyway.

Basically, it's the inline equivalent of what you suggested doing with the overriding the get_form method of ModelAdmin. Here we override get_formset in the inline class, get the form off the formset, and do the exact same thing. Seems to work fine, at least in 1.9, which I am using.

class VersionEntryInline(admin.TabularInline):
    template = 'admin/edit_inline/tabular_versionentry.html'
    model = VersionEntry
    extra = 0

    def get_formset(self, request, obj=None, **kwargs):
        """
        Override the formset function in order to remove the add and change buttons beside the foreign key pull-down
        menus in the inline.
        """
        formset = super(VersionEntryInline, self).get_formset(request, obj, **kwargs)
        form = formset.form
        widget = form.base_fields['project'].widget
        widget.can_add_related = False
        widget.can_change_related = False
        widget = form.base_fields['version'].widget
        widget.can_add_related = False
        widget.can_change_related = False
        return formset
thegiffman
  • 465
  • 4
  • 8
  • This worked for me in 1.8.3. Much nicer than the accepted answer imho. – jenniwren Aug 30 '16 at 22:14
  • Also works in 1.9.8. I guess a minimal example (based on above) would be: `formset.form.base_fields['my_field'].widget.can_add_related = False` (in between the call to `super` and the `return`) – djvg Jun 02 '17 at 13:41
  • Hmm. I just upgraded to 1.11 (on the way to 2.2!) and the `widget.can_add_related = False` line is causing page load to time out. When I comment it out it loads just fine. `widget.can_change_related = False` works as expected. Commenting in case anyone else has come across a fix for this!! – jenniwren Aug 16 '20 at 22:30
  • Curious: could it somehow be calling `widget.can_add_related = False` for every object in the list? It seems to be a problem when there are many items in the list but not when there are few. I have also just removed autocomplete_light which was in use on these fields previously and would have avoided long lists and thus hidden this issue in 1.8 where I'm upgrading from. – jenniwren Aug 16 '20 at 22:37
  • Worked for me in 3.1.2 – Nikita Tonkoskur Feb 25 '21 at 14:12
8

After a couple of intense days, I finally managed to find a way to achieve that.

A simple trick such as this is more than enough when dealing with this problem within ModelAdmin subclasses (see ClienteAdmin in my code above), so here's the class version without adding capabilities for "Prodotto" field:

@admin.register(Cliente)
class ClienteAdmin(admin.ModelAdmin):
    list_display = [
        'ragione_sociale', 'forma_societaria', 'titolare', 'partita_iva', ]
    list_filter = ['forma_societaria', ]
    search_fields = ['ragione_sociale', ]
    inlines = [RecapitoInline, SedeInline]
    def get_form(self, request, obj=None, **kwargs):    # Just added this override
        form = super(ClienteAdmin, self).get_form(request, obj, **kwargs)
        form.base_fields['prodotto'].widget.can_add_related = False
        return form

The real pain comes when dealing with inline classes (TabularInline, StackedInline), as the get_form() function seems not to be called at all, so the previous way won't work.

Explaining all my previous attempts would take too long, and I'm probably not even good enough with Django yet to tell why they didn't work. So let's get straight to the solution, which in fact is not even that complicated.

I subclassed django.contrib.admin.widgets.RelatedFieldWidgetWrapper widget and overridden its render method, so that it doesn't append the "add-another" anchor to the output. Easily done by commenting out a few lines. After doing so, monkeypatching the original RelatedFieldWidgetWrapper with my own version (django.contrib.admin.widgets.RelatedFieldWidgetWrapper = NoAddingRelatedFieldWidgetWrapper) made the trick.

Clearly, for it to work I had to add the import line in the admin.py:

from .widgets import NoAddingRelatedFieldWidgetWrapper

widgets.py

import django.contrib.admin.widgets
from django.utils.safestring import mark_safe


class NoAddingRelatedFieldWidgetWrapper(django.contrib.admin.widgets.RelatedFieldWidgetWrapper):

    def render(self, name, value, *args, **kwargs):
        from django.contrib.admin.views.main import TO_FIELD_VAR
        rel_to = self.rel.to
        info = (rel_to._meta.app_label, rel_to._meta.model_name)
        self.widget.choices = self.choices
        output = [self.widget.render(name, value, *args, **kwargs)]
        '''
        if self.can_add_related:
            related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
            url_params = '?%s=%s' % (TO_FIELD_VAR, self.rel.get_related_field().name)
            # TODO: "add_id_" is hard-coded here. This should instead use the
            # correct API to determine the ID dynamically.
            output.append('<a href="%s%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> '
                          % (related_url, url_params, name))
            output.append('<img src="%s" width="10" height="10" alt="%s"/></a>'
                          % (static('admin/img/icon_addlink.gif'), _('Add Another')))
        '''
        return mark_safe(''.join(output))

# Monkeypatch
django.contrib.admin.widgets.RelatedFieldWidgetWrapper = NoAddingRelatedFieldWidgetWrapper

For the sake of completion, here's the final version of the related admin.py:

admin.py

from django.contrib import admin
import django.contrib.admin.widgets

from django.db import models

from .models import Cliente, Prodotto, Sede
from apps.recapito.models import RecapitoCliente
from .widgets import NoAddingRelatedFieldWidgetWrapper


class SedeInline(admin.TabularInline):
    model = Sede
    extra = 1

    def provincia(self, obj):
        return obj.comune.provincia

    readonly_fields = ['provincia', ]


class RecapitoInline(admin.TabularInline):
    model = RecapitoCliente
    extra = 1
    readonly_fields = ['cliente', 'tipo', 'recapito', ]


@admin.register(Cliente)
class ClienteAdmin(admin.ModelAdmin):
    list_display = [
        'ragione_sociale', 'forma_societaria', 'titolare', 'partita_iva', ]
    list_filter = ['forma_societaria', ]
    search_fields = ['ragione_sociale', ]
    inlines = [RecapitoInline, SedeInline]
    def get_form(self, request, obj=None, **kwargs):
        form = super(ClienteAdmin, self).get_form(request, obj, **kwargs)
        form.base_fields['prodotto'].widget.can_add_related = False
        return form

Shall anyone come out with any better solution, I'll gladly accept it in place of mine.

Community
  • 1
  • 1
Seether
  • 1,524
  • 1
  • 14
  • 28
  • I can't help but feel that hiding it with javascript or css would be better - both for simplicity and maintainability. – Andy Baker May 17 '15 at 14:28
  • I think my answer below is the simpler solution you were looking for. Would you be willing to accept it? – thegiffman Jun 28 '16 at 03:44
  • @thegiffman as long as you're confident this works with Django 1.7 as per OP, I have no problem accepting your answer. I don't work with Django any longer though, so I can't give any more help in any way. – Seether Oct 03 '16 at 14:13
  • That should be the correct answer. Not sure why no one has appropriated yet – sheikhsalman08 Jul 11 '19 at 12:28
  • @sheikhsalman08 it has been the accepted answer (I had marked it myself) until other comments stated differently. As I'm not here to celebrate myself in any way (nor I can tell I'm a Django expert), I decided to change the e accepted answer. Still, you're free to choose the option that fits you best. – Seether Jul 11 '19 at 12:35
6

There is actually a clean solution for this:

class YourInline(admin.TabularInline):
    extra = 0
    max_num=0
lehoang
  • 3,577
  • 1
  • 15
  • 16