Skip to content

Commit

Permalink
feat: optional inline titles (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasvinclav authored Jul 23, 2024
1 parent de1361f commit 0cce931
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 95 deletions.
115 changes: 79 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,13 @@ Did you decide to start using Unfold but you don't have time to make the switch
- [Numeric filters](#numeric-filters)
- [Date/time filters](#datetime-filters)
- [Custom admin pages](#custom-admin-pages)
- [Nonrelated inlines](#nonrelated-inlines)
- [Display decorator](#display-decorator)
- [Change form tabs](#change-form-tabs)
- [Inlines](#inlines)
- [Custom title](#custom-title)
- [Hide title row](#hide-title-row)
- [Display as tabs](#display-as-tabs)
- [Nonrelated inlines](#nonrelated-inlines)
- [Third party packages](#third-party-packages)
- [django-celery-beat](#django-celery-beat)
- [django-guardian](#django-guardian)
Expand Down Expand Up @@ -751,41 +755,6 @@ The template is straightforward, extend from `unfold/layouts/base.html` and the
{% endblock %}
```

## Nonrelated inlines

To display inlines which are not related (no foreign key pointing at the main model) to the model instance in changeform, you can use nonrelated inlines which are included in `unfold.contrib.inlines` module. Make sure this module is included in `INSTALLED_APPS` in settings.py.

```python
from django.contrib.auth.models import User
from unfold.admin import ModelAdmin
from unfold.contrib.inlines.admin import NonrelatedTabularInline
from .models import OtherModel

class OtherNonrelatedInline(NonrelatedTabularInline): # NonrelatedStackedInline is available as well
model = OtherModel
fields = ["field1", "field2"] # Ignore property to display all fields

def get_form_queryset(self, obj):
"""
Gets all nonrelated objects needed for inlines. Method must be implemented.
"""
return self.model.objects.all()

def save_new_instance(self, parent, instance):
"""
Extra save method which can for example update inline instances based on current
main model object. Method must be implemented.
"""
pass


@admin.register(User)
class UserAdmin(ModelAdmin):
inlines = [OtherNonrelatedInline]
```

**NOTE:** credit for this functionality goes to [django-nonrelated-inlines](https://github.com/bhomnick/django-nonrelated-inlines)

## Display decorator

Unfold introduces it's own `unfold.decorators.display` decorator. By default it has exactly same behavior as native `django.contrib.admin.decorators.display` but it adds same customizations which helps to extends default logic.
Expand Down Expand Up @@ -912,6 +881,44 @@ class MyModelAdmin(ModelAdmin):
)
```

## Inlines

### Custom title

By default, the title available for each inline row is coming from the `__str__` implementation of the model. Unfold allows you to override this title by implementing `get_inline_title` on the model which can return your own custom title just for the inline.

```python
from unfold.admin import TabularInline


class User(models.Model):
# fiels, meta ...

def get_inline_title(self):
return "Custom title"


class MyInline(TabularInline):
model = User
```

### Hide title row

By applying `hide_title` attribute set to `True`, it is possible to hide the title row which is available for `StackedInline` or `TabularInline`. For `StackedInline` it is required to have disabled delete permission `can_delete` to be able to hide the title row, because the checkbox with the delete action is inside this title.

```python
# admin.py

from unfold.admin import TabularInline


class MyInline(TabularInline):
model = User
hide_title = True
```

### Display as tabs

Inlines can be grouped into tab navigation by specifying `tab` attribute in the inline class.

```python
Expand All @@ -925,6 +932,42 @@ class MyInline(TabularInline):
tab = True
```

### Nonrelated inlines

To display inlines which are not related (no foreign key pointing at the main model) to the model instance in changeform, you can use nonrelated inlines which are included in `unfold.contrib.inlines` module. Make sure this module is included in `INSTALLED_APPS` in settings.py.

```python
from django.contrib.auth.models import User
from unfold.admin import ModelAdmin
from unfold.contrib.inlines.admin import NonrelatedTabularInline
from .models import OtherModel

class OtherNonrelatedInline(NonrelatedTabularInline): # NonrelatedStackedInline is available as well
model = OtherModel
fields = ["field1", "field2"] # Ignore property to display all fields

def get_form_queryset(self, obj):
"""
Gets all nonrelated objects needed for inlines. Method must be implemented.
"""
return self.model.objects.all()

def save_new_instance(self, parent, instance):
"""
Extra save method which can for example update inline instances based on current
main model object. Method must be implemented.
"""
pass


@admin.register(User)
class UserAdmin(ModelAdmin):
inlines = [OtherNonrelatedInline]
```

**NOTE:** credit for this functionality goes to [django-nonrelated-inlines](https://github.com/bhomnick/django-nonrelated-inlines)


## Third party packages

### django-celery-beat
Expand Down
2 changes: 1 addition & 1 deletion src/unfold/static/unfold/css/styles.css

Large diffs are not rendered by default.

78 changes: 43 additions & 35 deletions src/unfold/templates/admin/edit_inline/stacked.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,60 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro
<div class="border border-gray-200 mb-6 overflow-hidden rounded-md shadow-sm text-gray-700 w-full dark:border-gray-800">
{% for inline_admin_form in inline_admin_formset %}
<div class="inline-related group inline-stacked {% if inline_admin_form.original or inline_admin_form.show_url %} has_original{% endif %}{% if forloop.last and inline_admin_formset.has_add_permission %} empty-form last-related{% endif %}" id="{{ inline_admin_formset.formset.prefix }}-{% if not forloop.last %}{{ forloop.counter0 }}{% else %}empty{% endif %}">
<h3 class="bg-gray-50 border-b {% if not forloop.first %}border-t{% endif %} border-gray-200 flex font-medium items-center mb-3 px-3 py-2 text-gray-400 text-sm dark:bg-white/[.02] dark:border-gray-800">
<span class="mr-2">
{{ inline_admin_formset.opts.verbose_name|capfirst }}:
</span>

<span class="inline_label font-semibold text-gray-900 dark:text-gray-200">
{% if inline_admin_form.original and inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %} font-medium text-primary-600 underline">
{{ inline_admin_form.original }}
</a>
{% else %}
{% if inline_admin_form.original %}
{{ inline_admin_form.original }}
{% if not inline_admin_formset.opts.hide_title or inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}
<h3 class="bg-gray-50 border-b {% if not forloop.first %}border-t{% endif %} border-gray-200 flex font-medium items-center mb-3 px-3 py-2 text-gray-400 text-sm dark:bg-white/[.02] dark:border-gray-800">
<span class="mr-2">
{{ inline_admin_formset.opts.verbose_name|capfirst }}:
</span>

{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }} font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% if inline_admin_formset.has_change_permission %}
{% translate "Change" %}
<span class="inline_label font-semibold text-gray-900 dark:text-gray-200">
{% if inline_admin_form.original and inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{% if inline_admin_formset.has_change_permission %}inlinechangelink{% else %}inlineviewlink{% endif %} font-medium text-primary-600 underline">
{{ inline_admin_form.original }}
</a>
{% else %}
{% if inline_admin_form.original %}
{% with inline_title=inline_admin_form.original.get_inline_title %}
{% if inline_title %}
{{ inline_title }}
{% else %}
{% translate "View" %}
{{ inline_admin_form.original }}
{% endif %}
</a>
{% endwith %}

{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }} font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% if inline_admin_formset.has_change_permission %}
{% translate "Change" %}
{% else %}
{% translate "View" %}
{% endif %}
</a>
{% endif %}
{% else %}
#{{ forloop.counter }}
{% endif %}
{% else %}
#{{ forloop.counter }}
{% endif %}
{% endif %}
</span>
</span>

{% if inline_admin_form.show_url %}
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% trans "View on site" %}
</a>
{% endif %}
{% if inline_admin_form.show_url %}
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% trans "View on site" %}
</a>
{% endif %}

{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}
<span class="delete flex gap-2 items-center ml-auto text-gray-500">
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }} {{ inline_admin_form.deletion_field.label_tag }}
</span>
{% endif %}
</h3>
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}
<span class="delete flex gap-2 items-center ml-auto text-gray-500">
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }} {{ inline_admin_form.deletion_field.label_tag }}
</span>
{% endif %}
</h3>
{% endif %}

{% include "unfold/helpers/messages/error.html" with errors=inline_admin_form.form.non_field_errors %}

{% for fieldset in inline_admin_form %}
<div class="px-3 -mb-5">
<div class="px-3 -mb-5 {% if inline_admin_formset.opts.hide_title %}{% if not inline_admin_formset.formset.can_delete or not inline_admin_formset.has_delete_permission %}pt-3{% endif %}{% endif %}">
{% include 'admin/includes/fieldset.html' with stacked=1 %}
</div>
{% endfor %}
Expand Down
54 changes: 31 additions & 23 deletions src/unfold/templates/admin/edit_inline/tabular.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,31 +59,39 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro

{% if inline_admin_form.original or inline_admin_form.show_url %}
<tr>
<td class="original {% if inline_admin_form.original or inline_admin_form.show_url %}border-b border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-white/[.02]{% endif %}" colspan="{{ inline_admin_form|cell_count }}">
{% if inline_admin_form.original or inline_admin_form.show_url %}
<p class="bg-gray-50 flex font-medium items-center px-3 py-2 text-gray-400 text-sm dark:bg-white/[.02] ">
{% if inline_admin_form.original %}
<span class="font-semibold text-gray-900 dark:text-gray-200">
{{ inline_admin_form.original }}
</span>

{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }} font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% if inline_admin_formset.has_change_permission %}
{% translate "Change" %}
{% else %}
{% translate "View" %}
{% endif %}
</a>
<td class="original {% if not inline_admin_formset.opts.hide_title %}{% if inline_admin_form.original or inline_admin_form.show_url %}border-b border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-white/[.02]{% endif %}{% endif %}" colspan="{{ inline_admin_form|cell_count }}">
{% if not inline_admin_formset.opts.hide_title %}
{% if inline_admin_form.original or inline_admin_form.show_url %}
<p class="bg-gray-50 flex font-medium items-center px-3 py-2 text-gray-400 text-sm dark:bg-white/[.02] ">
{% if inline_admin_form.original %}
<span class="font-semibold text-gray-900 dark:text-gray-200">
{% with inline_title=inline_admin_form.original.get_inline_title %}
{% if inline_title %}
{{ inline_title }}
{% else %}
{{ inline_admin_form.original }}
{% endif %}
{% endwith %}
</span>

{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
<a href="{% url inline_admin_form.model_admin.opts|admin_urlname:'change' inline_admin_form.original.pk|admin_urlquote %}" class="{{ inline_admin_formset.has_change_permission|yesno:'inlinechangelink,inlineviewlink' }} font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% if inline_admin_formset.has_change_permission %}
{% translate "Change" %}
{% else %}
{% translate "View" %}
{% endif %}
</a>
{% endif %}
{% endif %}
{% endif %}

{% if inline_admin_form.show_url %}
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% translate "View on site" %}
</a>
{% endif %}
</p>
{% if inline_admin_form.show_url %}
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
{% translate "View on site" %}
</a>
{% endif %}
</p>
{% endif %}
{% endif %}

{% if inline_admin_form.needs_explicit_pk_field %}
Expand Down

0 comments on commit 0cce931

Please sign in to comment.