Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

has_delete_permission gets parent instance in django admin inline

I have a Booking model with a User foreign key. In the admin the bookings are inlined inside the user change page.

I want to prevent some bookings from being deleted (from the inline) when there are less then 24 hours before the booking AND the logged user is not in SuperStaff group.

So I define the BookingInline something like that:

class BookingInline(admin.TabularInline):
    model = Booking
    extra = 0
    fk_name = 'bookedFor'

    def has_delete_permission(self, request, obj=None):
        if not request.user.profile.isSuperStaff() and obj.is24hoursFromNow():
            return True
        return False

This code is reached, but I get a User instance, instead of a Booking one (and an error, of course), thus cannot decide for each inlined booking if it could be deleted or not. Isn't the has_delete_permission() method supposed to get the inlined object instance in this case? There is nothing about in the django docs...

I know the code is reached since I checked it using only the condition on user, and it actually hides the delete box for appropriate users.

I also tried to do it other way, through the Formset and clean() method, but it doesn't have the request parameter, so I get the desired instance, but not the user logged in.

I've searched for a solution for a few hours, but seems like the only way is to put a link from the inline to the full change page of a Booking object, and check the permissions when a user will attempt to regularly delete a Booking.

Any ideas how can that be done in an elegant way would be appreciated.

like image 643
olessia Avatar asked Jan 24 '15 20:01

olessia


2 Answers

I was facing exactly the same problem today, and I think I've found an acceptable way to solve it. Here's what I did:

I had to make inlines deletable only if a particular field had a certain value. Specifically, as I'm dealing with generic tasks and assignments, only non-accepted tasks have to be deletable. In model terms:

class Task(models.Model):
    STATUS_CHOICES = (
        ('PND', 'Pending'),
        ('ACC', 'Accepted'),
    )
    status = models.CharField(       ----> If this != 'PND', inline instance 
        max_length=3,                      should not be deletable
        choices=STATUS_CHOICES,
        default=STATUS_CHOICES[0][0])

Since I couldn't use has_delete_permission within my admin.TabularInline class either, as it refers to the whole fieldset (i.e. all the inlines) and not to the single row, I went through the path of template overriding:

tabular.html:44-62 (original)

[...]
{% for fieldset in inline_admin_form %}
  {% for line in fieldset %}
    {% for field in line %}
      {% if not field.field.is_hidden %}
      <td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
      {% if field.is_readonly %}
          <p>{{ field.contents }}</p>
      {% else %}
          {{ field.field.errors.as_ul }}
          {{ field.field }}
      {% endif %}
      </td>
      {% endif %}
    {% endfor %}
  {% endfor %}
{% endfor %}
{% if inline_admin_formset.formset.can_delete %}
  <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
{% endif %}
[...]

tabular.html (overridden)

[...]
{% for fieldset in inline_admin_form %}
  {% for line in fieldset %}
    {% for field in line %}
      {% if not field.field.is_hidden %}
      <td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
      {% if field.is_readonly %}
        <p>{{ field.contents }}</p>
      {% else %}
        {% include "admin/includes/field.html" with is_tabular=True %}
      {% endif %}
      </td>
      {% endif %}
    {% endfor %}
  {% endfor %}

  <!-- Custom deletion logic, only available for non-accepted objects -->
  {% for line in fieldset %}
    {% for field in line %}
      {% if field.field.name == "status" %}
        {% if field.field.value == "PND" %}
          <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
        {% else %}
          <td class="delete"><input type="checkbox" disabled="disabled">
          <img src="/static/admin/img/icon_alert.gif" data-toggle="tooltip" class="title-starter"
          data-original-title="Can't remove accepted tasks" />
          </td>
        {% endif %}
      {% endif %}
    {% endfor %}
  {% endfor %}

{% endfor %}

<!-- Classic deletion, removed
{% if inline_admin_formset.formset.can_delete %}
  <td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
{% endif %}
-->
[...]

As a result (non-standard graphic as I'm using django-admin-bootstrap):

enter image description here

Strictly talking about "elegance", I have to get through the lines' fields twice to make it work, but I haven't found any better way like directly reading that field's value. I couldn't have anything like {{ line.fields.0.status }} or {{ line.fields.status }} work. If anyone could point to the direct syntax, I'd gladly update my solution.

Anyway, since it still works and it's not really that bad, I'll be fine with this method until anything clearly better comes out.

like image 109
Seether Avatar answered Oct 16 '22 06:10

Seether


You can check conditions in formset's clean() method.

class BookingFormSet(forms.BaseInlineFormSet):
    def clean(self):
        super().clean()
        has_errors = False
        for form in self.deleted_forms:
            if form.instance.is24hoursFromNow():
                form._errors[NON_FIELD_ERRORS] = self.error_class(['Not allowed to delete'])
                has_errors = True

        if has_errors:
            raise forms.ValidationError('Please correct the errors below')

class BookingInline(admin.TabularInline):
    model = Booking
    formset = BookingFormSet

Note that you don't have request object here, so can't check for isSuperStaff()

like image 2
Ivan Virabyan Avatar answered Oct 16 '22 05:10

Ivan Virabyan