Skip to content

Commit 0cce931

Browse files
authored
feat: optional inline titles (#619)
1 parent de1361f commit 0cce931

File tree

4 files changed

+154
-95
lines changed

4 files changed

+154
-95
lines changed

README.md

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,13 @@ Did you decide to start using Unfold but you don't have time to make the switch
6161
- [Numeric filters](#numeric-filters)
6262
- [Date/time filters](#datetime-filters)
6363
- [Custom admin pages](#custom-admin-pages)
64-
- [Nonrelated inlines](#nonrelated-inlines)
6564
- [Display decorator](#display-decorator)
6665
- [Change form tabs](#change-form-tabs)
66+
- [Inlines](#inlines)
67+
- [Custom title](#custom-title)
68+
- [Hide title row](#hide-title-row)
69+
- [Display as tabs](#display-as-tabs)
70+
- [Nonrelated inlines](#nonrelated-inlines)
6771
- [Third party packages](#third-party-packages)
6872
- [django-celery-beat](#django-celery-beat)
6973
- [django-guardian](#django-guardian)
@@ -751,41 +755,6 @@ The template is straightforward, extend from `unfold/layouts/base.html` and the
751755
{% endblock %}
752756
```
753757

754-
## Nonrelated inlines
755-
756-
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.
757-
758-
```python
759-
from django.contrib.auth.models import User
760-
from unfold.admin import ModelAdmin
761-
from unfold.contrib.inlines.admin import NonrelatedTabularInline
762-
from .models import OtherModel
763-
764-
class OtherNonrelatedInline(NonrelatedTabularInline): # NonrelatedStackedInline is available as well
765-
model = OtherModel
766-
fields = ["field1", "field2"] # Ignore property to display all fields
767-
768-
def get_form_queryset(self, obj):
769-
"""
770-
Gets all nonrelated objects needed for inlines. Method must be implemented.
771-
"""
772-
return self.model.objects.all()
773-
774-
def save_new_instance(self, parent, instance):
775-
"""
776-
Extra save method which can for example update inline instances based on current
777-
main model object. Method must be implemented.
778-
"""
779-
pass
780-
781-
782-
@admin.register(User)
783-
class UserAdmin(ModelAdmin):
784-
inlines = [OtherNonrelatedInline]
785-
```
786-
787-
**NOTE:** credit for this functionality goes to [django-nonrelated-inlines](https://github.com/bhomnick/django-nonrelated-inlines)
788-
789758
## Display decorator
790759

791760
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.
@@ -912,6 +881,44 @@ class MyModelAdmin(ModelAdmin):
912881
)
913882
```
914883

884+
## Inlines
885+
886+
### Custom title
887+
888+
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.
889+
890+
```python
891+
from unfold.admin import TabularInline
892+
893+
894+
class User(models.Model):
895+
# fiels, meta ...
896+
897+
def get_inline_title(self):
898+
return "Custom title"
899+
900+
901+
class MyInline(TabularInline):
902+
model = User
903+
```
904+
905+
### Hide title row
906+
907+
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.
908+
909+
```python
910+
# admin.py
911+
912+
from unfold.admin import TabularInline
913+
914+
915+
class MyInline(TabularInline):
916+
model = User
917+
hide_title = True
918+
```
919+
920+
### Display as tabs
921+
915922
Inlines can be grouped into tab navigation by specifying `tab` attribute in the inline class.
916923

917924
```python
@@ -925,6 +932,42 @@ class MyInline(TabularInline):
925932
tab = True
926933
```
927934

935+
### Nonrelated inlines
936+
937+
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.
938+
939+
```python
940+
from django.contrib.auth.models import User
941+
from unfold.admin import ModelAdmin
942+
from unfold.contrib.inlines.admin import NonrelatedTabularInline
943+
from .models import OtherModel
944+
945+
class OtherNonrelatedInline(NonrelatedTabularInline): # NonrelatedStackedInline is available as well
946+
model = OtherModel
947+
fields = ["field1", "field2"] # Ignore property to display all fields
948+
949+
def get_form_queryset(self, obj):
950+
"""
951+
Gets all nonrelated objects needed for inlines. Method must be implemented.
952+
"""
953+
return self.model.objects.all()
954+
955+
def save_new_instance(self, parent, instance):
956+
"""
957+
Extra save method which can for example update inline instances based on current
958+
main model object. Method must be implemented.
959+
"""
960+
pass
961+
962+
963+
@admin.register(User)
964+
class UserAdmin(ModelAdmin):
965+
inlines = [OtherNonrelatedInline]
966+
```
967+
968+
**NOTE:** credit for this functionality goes to [django-nonrelated-inlines](https://github.com/bhomnick/django-nonrelated-inlines)
969+
970+
928971
## Third party packages
929972

930973
### django-celery-beat

src/unfold/static/unfold/css/styles.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/unfold/templates/admin/edit_inline/stacked.html

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,52 +16,60 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro
1616
<div class="border border-gray-200 mb-6 overflow-hidden rounded-md shadow-sm text-gray-700 w-full dark:border-gray-800">
1717
{% for inline_admin_form in inline_admin_formset %}
1818
<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 %}">
19-
<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">
20-
<span class="mr-2">
21-
{{ inline_admin_formset.opts.verbose_name|capfirst }}:
22-
</span>
23-
24-
<span class="inline_label font-semibold text-gray-900 dark:text-gray-200">
25-
{% if inline_admin_form.original and inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
26-
<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">
27-
{{ inline_admin_form.original }}
28-
</a>
29-
{% else %}
30-
{% if inline_admin_form.original %}
31-
{{ inline_admin_form.original }}
19+
{% if not inline_admin_formset.opts.hide_title or inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission %}
20+
<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">
21+
<span class="mr-2">
22+
{{ inline_admin_formset.opts.verbose_name|capfirst }}:
23+
</span>
3224

33-
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
34-
<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">
35-
{% if inline_admin_formset.has_change_permission %}
36-
{% translate "Change" %}
25+
<span class="inline_label font-semibold text-gray-900 dark:text-gray-200">
26+
{% if inline_admin_form.original and inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
27+
<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">
28+
{{ inline_admin_form.original }}
29+
</a>
30+
{% else %}
31+
{% if inline_admin_form.original %}
32+
{% with inline_title=inline_admin_form.original.get_inline_title %}
33+
{% if inline_title %}
34+
{{ inline_title }}
3735
{% else %}
38-
{% translate "View" %}
36+
{{ inline_admin_form.original }}
3937
{% endif %}
40-
</a>
38+
{% endwith %}
39+
40+
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
41+
<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">
42+
{% if inline_admin_formset.has_change_permission %}
43+
{% translate "Change" %}
44+
{% else %}
45+
{% translate "View" %}
46+
{% endif %}
47+
</a>
48+
{% endif %}
49+
{% else %}
50+
#{{ forloop.counter }}
4151
{% endif %}
42-
{% else %}
43-
#{{ forloop.counter }}
4452
{% endif %}
45-
{% endif %}
46-
</span>
53+
</span>
4754

48-
{% if inline_admin_form.show_url %}
49-
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
50-
{% trans "View on site" %}
51-
</a>
52-
{% endif %}
55+
{% if inline_admin_form.show_url %}
56+
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
57+
{% trans "View on site" %}
58+
</a>
59+
{% endif %}
5360

54-
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}
55-
<span class="delete flex gap-2 items-center ml-auto text-gray-500">
56-
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }} {{ inline_admin_form.deletion_field.label_tag }}
57-
</span>
58-
{% endif %}
59-
</h3>
61+
{% if inline_admin_formset.formset.can_delete and inline_admin_formset.has_delete_permission and inline_admin_form.original %}
62+
<span class="delete flex gap-2 items-center ml-auto text-gray-500">
63+
{{ inline_admin_form.deletion_field.field|add_css_class:form_classes.checkbox }} {{ inline_admin_form.deletion_field.label_tag }}
64+
</span>
65+
{% endif %}
66+
</h3>
67+
{% endif %}
6068

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

6371
{% for fieldset in inline_admin_form %}
64-
<div class="px-3 -mb-5">
72+
<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 %}">
6573
{% include 'admin/includes/fieldset.html' with stacked=1 %}
6674
</div>
6775
{% endfor %}

src/unfold/templates/admin/edit_inline/tabular.html

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,39 @@ <h2 class="bg-gray-100 border border-transparent font-semibold mb-6 px-4 py-3 ro
5959

6060
{% if inline_admin_form.original or inline_admin_form.show_url %}
6161
<tr>
62-
<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 }}">
63-
{% if inline_admin_form.original or inline_admin_form.show_url %}
64-
<p class="bg-gray-50 flex font-medium items-center px-3 py-2 text-gray-400 text-sm dark:bg-white/[.02] ">
65-
{% if inline_admin_form.original %}
66-
<span class="font-semibold text-gray-900 dark:text-gray-200">
67-
{{ inline_admin_form.original }}
68-
</span>
69-
70-
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
71-
<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">
72-
{% if inline_admin_formset.has_change_permission %}
73-
{% translate "Change" %}
74-
{% else %}
75-
{% translate "View" %}
76-
{% endif %}
77-
</a>
62+
<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 }}">
63+
{% if not inline_admin_formset.opts.hide_title %}
64+
{% if inline_admin_form.original or inline_admin_form.show_url %}
65+
<p class="bg-gray-50 flex font-medium items-center px-3 py-2 text-gray-400 text-sm dark:bg-white/[.02] ">
66+
{% if inline_admin_form.original %}
67+
<span class="font-semibold text-gray-900 dark:text-gray-200">
68+
{% with inline_title=inline_admin_form.original.get_inline_title %}
69+
{% if inline_title %}
70+
{{ inline_title }}
71+
{% else %}
72+
{{ inline_admin_form.original }}
73+
{% endif %}
74+
{% endwith %}
75+
</span>
76+
77+
{% if inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
78+
<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">
79+
{% if inline_admin_formset.has_change_permission %}
80+
{% translate "Change" %}
81+
{% else %}
82+
{% translate "View" %}
83+
{% endif %}
84+
</a>
85+
{% endif %}
7886
{% endif %}
79-
{% endif %}
8087

81-
{% if inline_admin_form.show_url %}
82-
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
83-
{% translate "View on site" %}
84-
</a>
85-
{% endif %}
86-
</p>
88+
{% if inline_admin_form.show_url %}
89+
<a href="{{ inline_admin_form.absolute_url }}" class="font-medium ml-2 text-primary-600 underline dark:text-primary-500">
90+
{% translate "View on site" %}
91+
</a>
92+
{% endif %}
93+
</p>
94+
{% endif %}
8795
{% endif %}
8896

8997
{% if inline_admin_form.needs_explicit_pk_field %}

0 commit comments

Comments
 (0)