Created
February 9, 2025 02:25
-
-
Save dacodekid/a21f08b97d37f3bc6e6afab9750b6dc2 to your computer and use it in GitHub Desktop.
Add navigation buttons for `django-unfold`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
backend/apps/core/mixins.py | |
""" | |
from urllib.parse import parse_qsl | |
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper | |
from django.db import models | |
from django.utils.safestring import mark_safe | |
from guardian.admin import GuardedModelAdmin | |
from import_export.admin import ImportExportModelAdmin | |
from simple_history.admin import SimpleHistoryAdmin | |
from unfold.admin import ModelAdmin, StackedInline, TabularInline | |
from unfold.contrib.import_export.forms import ImportForm, SelectableFieldsExportForm | |
from unfold.decorators import display | |
from unfold.widgets import UnfoldAdminSingleDateWidget | |
from .widgets import WysiwygWidget | |
class SoeAdminMixin(ModelAdmin, SimpleHistoryAdmin, GuardedModelAdmin, ImportExportModelAdmin): | |
""" | |
This class represents a base admin interface mixin. | |
""" | |
list_per_page = 200 | |
import_form_class = ImportForm | |
export_form_class = SelectableFieldsExportForm | |
list_fullwidth = True | |
warn_unsaved_form = True | |
formfield_overrides = { | |
models.TextField: { | |
"widget": WysiwygWidget, | |
}, | |
models.DateField: { | |
"widget": UnfoldAdminSingleDateWidget( | |
# format="%m-%d-%Y", | |
attrs={"placeholder": "MM-DD-YYYY"}, | |
), | |
# "input_formats": ["%m-%d-%Y"], | |
}, | |
} | |
@display(description="Notes", ordering="notes") | |
def display_notes(self, obj): | |
"""Display notes as HTML in list view.""" | |
if obj.notes: | |
return mark_safe(obj.notes) | |
return "-" | |
@display(description="Address", ordering="address") | |
def display_address(self, obj): | |
"""Display address as HTML in list view.""" | |
if obj.address: | |
return mark_safe(obj.address) | |
return "-" | |
def get_form(self, request, obj=None, **kwargs): | |
form = super().get_form(request, obj, **kwargs) | |
for _field_name, form_field in form.base_fields.items(): | |
widget = form_field.widget | |
# Check if the widget is a RelatedFieldWidgetWrapper instance | |
if isinstance(widget, RelatedFieldWidgetWrapper): | |
widget.can_add_related = False | |
widget.can_change_related = False | |
widget.can_delete_related = False | |
widget.can_view_related = False | |
return form | |
def change_view(self, request, object_id, form_url="", extra_context=None): | |
extra_context = extra_context or {} | |
obj = self.get_object(request, object_id) | |
# Get the base queryset | |
qs = self.get_queryset(request) | |
# Get and parse filters from the request | |
changelist_filters = request.GET.get("_changelist_filters", "") | |
if changelist_filters: | |
try: | |
# Parse the URL-encoded filter string | |
filter_dict = dict(parse_qsl(changelist_filters)) | |
# Apply each filter to the queryset | |
qs = qs.filter(**filter_dict) | |
except Exception as e: | |
# Log error but continue with unfiltered queryset | |
print(f"Error applying filters: {e}") | |
# Get the ordering from the request | |
ordering = request.GET.get("o") # Django's default ordering parameter | |
if ordering: | |
try: | |
# Convert ordering parameter to field names | |
order_fields = [] | |
for field_index in ordering.split("."): | |
try: | |
index = int(field_index) | |
# Get the actual field from list_display | |
field = self.get_list_display(request)[abs(index)] | |
# If index is negative, it's descending order | |
if index < 0: | |
field = "-" + field | |
order_fields.append(field) | |
except (ValueError, IndexError): | |
continue | |
if order_fields: | |
qs = qs.order_by(*order_fields) | |
except Exception as e: | |
print(f"Error applying ordering: {e}") | |
else: | |
# Use default ordering from model or admin | |
ordering = self.get_ordering(request) or () | |
qs = qs.order_by(*ordering) if ordering else qs.order_by("name") | |
# Get the list of IDs in the correct order | |
ordered_ids = list(qs.values_list("id", flat=True)) | |
try: | |
# Find current position | |
current_position = ordered_ids.index(obj.pk) | |
# Get prev/next objects | |
prev_obj = None if current_position == 0 else self.model.objects.get(pk=ordered_ids[current_position - 1]) | |
next_obj = None if current_position == len(ordered_ids) - 1 else self.model.objects.get(pk=ordered_ids[current_position + 1]) | |
except (ValueError, IndexError): | |
prev_obj = next_obj = None | |
# Add navigation context | |
extra_context.update({ | |
"prev_obj": prev_obj, | |
"next_obj": next_obj, | |
"has_prev": bool(prev_obj), | |
"has_next": bool(next_obj), | |
"opts": self.model._meta, | |
"changelist_filters": changelist_filters, | |
}) | |
return super().change_view(request, object_id, form_url, extra_context=extra_context) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% load i18n admin_urls %} | |
<div {% if not is_popup %}id="submit-row"{% endif %} class="relative {% if not is_popup %}lg:mt-16{% endif %} z-20"> | |
<div class="{% if not is_popup %}max-w-full lg:bottom-0 lg:fixed lg:left-0 lg:right-0{% endif %}" {% if not is_popup %}x-bind:class="{'xl:left-0': !sidebarDesktopOpen, 'xl:left-72': sidebarDesktopOpen}" x-bind:style="'width: ' + mainWidth + 'px'"{% endif %}> | |
<div class="backdrop-blur-sm bg-white/80 pb-4 dark:bg-base-900/80 {% if not is_popup %}lg:border-t lg:border-base-200 lg:py-4 relative lg:scrollable-top lg:px-8 dark:border-base-800{% endif %}"> | |
<div class="flex flex-col-reverse gap-3 items-center mx-auto lg:flex-row-reverse"> | |
{% block submit-row %} | |
{% if show_save %} | |
<button type="submit" name="_save" class="bg-primary-600 block border border-transparent font-medium px-3 py-2 rounded text-white w-full lg:w-auto"> | |
{% translate 'Save' %} | |
</button> | |
{% endif %} | |
{% for action in actions_submit_line %} | |
<button type="submit" {% if not action.attrs.name %}name="{{ action.action_name }}"{% endif %} class="border border-base-200 font-medium px-3 py-2 rounded transition-all w-full hover:bg-base-50 lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900" {% include "unfold/helpers/attrs.html" with attrs=action.attrs %}> | |
{{ action.description }} | |
</button> | |
{% endfor %} | |
{% if show_save_and_continue %} | |
<button type="submit" name="_continue" class="border border-base-200 font-medium px-3 py-2 rounded transition-all w-full hover:bg-base-50 lg:block lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900"> | |
{% if can_change %} | |
{% translate 'Save and continue editing' %} | |
{% else %} | |
{% translate 'Save and view' %} | |
{% endif %} | |
</button> | |
{% endif %} | |
{% if show_save_and_add_another %} | |
<button type="submit" name="_addanother" class="border border-base-200 font-medium px-3 py-2 rounded transition-all w-full hover:bg-base-50 lg:block lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900"> | |
{% translate 'Save and add another' %} | |
</button> | |
{% endif %} | |
{% if show_save_as_new %} | |
<button type="submit" name="_saveasnew" class="border border-base-200 font-medium px-3 py-2 rounded transition-all w-full hover:bg-base-50 lg:block lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900"> | |
{% translate 'Save as new' %} | |
</button> | |
{% endif %} | |
<!-- Navigation Buttons --> | |
<div class="flex items-center gap-2 mx-auto lg:mx-4"> | |
{% url opts|admin_urlname:'change' prev_obj.pk as prev_url %} | |
{% url opts|admin_urlname:'change' next_obj.pk as next_url %} | |
<a href="{% if has_prev %}{{ prev_url }}{% if changelist_filters %}?_changelist_filters={{ changelist_filters }}{% endif %}{% else %}#{% endif %}" | |
class="border border-base-200 font-medium px-3 py-2 rounded transition-all hover:bg-base-50 lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900 {% if not has_prev %}opacity-50 cursor-not-allowed pointer-events-none{% endif %}"> | |
Previous | |
</a> | |
<a href="{% if has_next %}{{ next_url }}{% if changelist_filters %}?_changelist_filters={{ changelist_filters }}{% endif %}{% else %}#{% endif %}" | |
class="border border-base-200 font-medium px-3 py-2 rounded transition-all hover:bg-base-50 lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900 {% if not has_next %}opacity-50 cursor-not-allowed pointer-events-none{% endif %}"> | |
Next | |
</a> | |
</div> | |
<div class="flex flex-col gap-3 mr-auto w-full lg:flex-row lg:w-auto"> | |
{% if show_close or adminform.model_admin.change_form_show_cancel_button %} | |
{% url opts|admin_urlname:'changelist' as changelist_url %} | |
<a href="{{ changelist_url }}{% if changelist_filters %}?_changelist_filters={{ changelist_filters }}{% endif %}" class="border border-base-200 font-medium px-3 py-2 rounded text-center transition-all w-full hover:bg-base-50 lg:block lg:w-auto dark:border-base-700 dark:hover:text-base-200 dark:hover:bg-base-900"> | |
{% translate 'Close' %} | |
</a> | |
{% endif %} | |
{% if show_delete_link and original %} | |
{% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} | |
<a href="{{ delete_url }}{% if changelist_filters %}?_changelist_filters={{ changelist_filters }}{% endif %}" class="bg-red-600 flex items-center justify-center font-medium h-9.5 ml-auto px-3 py-2 rounded text-center text-white w-full lg:w-auto dark:bg-red-500/20 dark:text-red-500"> | |
{% translate "Delete" %} {{ opts.verbose_name }} | |
</a> | |
{% endif %} | |
</div> | |
{% endblock %} | |
</div> | |
</div> | |
</div> | |
</div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment