Skip to content

Instantly share code, notes, and snippets.

@dacodekid
Created February 9, 2025 02:25
Show Gist options
  • Save dacodekid/a21f08b97d37f3bc6e6afab9750b6dc2 to your computer and use it in GitHub Desktop.
Save dacodekid/a21f08b97d37f3bc6e6afab9750b6dc2 to your computer and use it in GitHub Desktop.
Add navigation buttons for `django-unfold`
"""
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)
{% 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