Browse Source

feat: add Django Unfold admin configurations for applications and custom base template

main
sina_sajjadi 3 days ago
parent
commit
98bcc2f2a6
  1. 1
      apps/account/admin/location.py
  2. 1
      apps/account/admin/login_history.py
  3. 2
      apps/account/admin/notification.py
  4. 1
      apps/account/admin/role.py
  5. 6
      apps/account/admin/user.py
  6. 2
      apps/api/admin.py
  7. 2
      apps/chat/admin.py
  8. 6
      apps/cms/admin.py
  9. 1
      apps/diplomacy/admin.py
  10. 2
      apps/donations/admin.py
  11. 2
      apps/dynamic_forms/admin.py
  12. 2
      apps/events/admin.py
  13. 7
      apps/lms/admin.py
  14. 1
      apps/meetings/admin.py
  15. 5
      apps/profiles/admin.py
  16. 4
      apps/projects/admin.py
  17. 2
      apps/tickets/admin.py
  18. 4
      config/settings/base.py
  19. 200
      templates/admin/base_site.html
  20. 25
      utils/admin.py

1
apps/account/admin/location.py

@ -26,6 +26,7 @@ class LocationHistoryInline(TabularInline):
class LocationHistoryAdmin(ModelAdmin):
list_display = ('user', 'display_location', 'country', 'city', 'selected_manually', 'ip', 'display_at_time')
list_display_links = ('user', 'display_location', 'country', 'city')
list_filter = [
('user', AutocompleteSelectFilter),
'country',

1
apps/account/admin/login_history.py

@ -11,6 +11,7 @@ from utils.admin import project_admin_site
@admin.register(LoginHistory, site=project_admin_site)
class LoginHistoryAdmin(ModelAdmin):
list_display = ('user', 'ip', 'country', 'city', 'device_os', 'display_at_time')
list_display_links = ('user', 'ip', 'display_at_time')
list_filter = [
('user', AutocompleteSelectFilter),
'device_os',

2
apps/account/admin/notification.py

@ -9,6 +9,7 @@ from apps.account.models import User, Notification, NotificationTemplate
@admin.register(Notification, site=project_admin_site)
class NotificationAdmin(ModelAdmin):
list_display = ('title', 'user', 'notification_type', 'is_read', 'created_at')
list_display_links = ('title', 'user')
list_filter = ('is_read', 'notification_type', 'created_at')
search_fields = ('title', 'message', 'user__fullname', 'user__email')
list_editable = ('is_read',)
@ -19,6 +20,7 @@ class NotificationAdmin(ModelAdmin):
@admin.register(NotificationTemplate, site=project_admin_site)
class NotificationTemplateAdmin(ModelAdmin):
list_display = ('name', 'notification_type', 'title', 'is_active')
list_display_links = ('name', 'title')
list_filter = ('is_active',)
search_fields = ('name', 'notification_type', 'title', 'body')
list_editable = ('is_active',)

1
apps/account/admin/role.py

@ -9,6 +9,7 @@ from utils.admin import project_admin_site
class RoleAdmin(ModelAdmin):
list_display = ('name', 'slug', 'is_default', 'is_system', 'users_count', 'updated_at')
list_display_links = ('name', 'slug')
list_filter = ('is_default', 'is_system')
search_fields = ('name', 'slug', 'description')
ordering = ('name',)

6
apps/account/admin/user.py

@ -56,8 +56,10 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
change_password_form = AdminPasswordChangeForm
compressed_fields = False
list_display = ('first_name', 'last_name', 'fullname', 'email', 'role', 'user_type', 'is_staff', 'is_active', 'display_date_joined',)
list_display_links = ('first_name', 'last_name', 'fullname', 'email')
ordering = ("-id",)
search_fields = ('email', 'first_name', 'last_name', 'fullname', 'username',)
filter_horizontal = ('groups', 'user_permissions')
list_filter = [
"role",
"user_type",
@ -114,7 +116,8 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
"fields": (
("role", "user_type"),
("is_active", "is_staff", "is_superuser"),
("groups", "user_permissions"),
"groups",
"user_permissions",
),
}),
(_("Custom Permissions"), {
@ -142,6 +145,7 @@ class UserAdmin(ModelAdmin, BaseUserAdmin):
class GroupAdmin(BaseGroupAdmin, ModelAdmin):
list_display = ('name', 'permissions_count')
list_display_links = ('name',)
search_fields = ('name',)
ordering = ('name',)
filter_horizontal = ('permissions',)

2
apps/api/admin.py

@ -10,6 +10,7 @@ from utils.admin import project_admin_site
@admin.register(AppVersion)
class AppVersionAdmin(ModelAdmin):
list_display = ['version', 'app_type', 'is_active', 'downloads_count', 'created_at']
list_display_links = ['version', 'app_type']
list_filter = ['app_type', 'is_active']
search_fields = ['version', 'description']
ordering = ['-created_at']
@ -18,6 +19,7 @@ class AppVersionAdmin(ModelAdmin):
@admin.register(SupportMessage)
class SupportMessageAdmin(ModelAdmin):
list_display = ['sender_name', 'sender_email', 'subject', 'is_resolved', 'created_at']
list_display_links = ['sender_name', 'sender_email', 'subject']
list_filter = ['is_resolved', 'created_at']
search_fields = ['sender_name', 'sender_email', 'subject', 'message']
ordering = ['-created_at']

2
apps/chat/admin.py

@ -17,6 +17,7 @@ class ChatMessageInline(TabularInline):
@admin.register(ChatRoom, site=project_admin_site)
class ChatRoomAdmin(ModelAdmin):
list_display = ('id', 'title_display', 'room_type_badge', 'messages_count', 'created_at', 'updated_at')
list_display_links = ('id', 'title_display')
list_filter = ('room_type', 'created_at')
search_fields = ('title', 'participant_users__email', 'participant_institutions__name')
filter_horizontal = ('participant_users', 'participant_institutions')
@ -42,6 +43,7 @@ class ChatRoomAdmin(ModelAdmin):
@admin.register(ChatMessage, site=project_admin_site)
class ChatMessageAdmin(ModelAdmin):
list_display = ('id', 'room', 'sender', 'sender_institution', 'content_preview', 'has_attachment', 'created_at')
list_display_links = ('id', 'room', 'sender', 'content_preview')
list_filter = ('created_at', 'sender_institution')
search_fields = ('content', 'sender__email', 'sender__fullname', 'room__title')
readonly_fields = ('created_at',)

6
apps/cms/admin.py

@ -35,6 +35,7 @@ class PostAdmin(ModelAdmin):
'is_featured',
'publish_date',
)
list_display_links = ('title', 'post_type_badge', 'category', 'author')
list_filter = ('post_type', 'language', 'status', 'is_featured', 'category')
search_fields = ('title', 'excerpt', 'content', 'author__email', 'author__fullname')
prepopulated_fields = {'slug': ('title',)}
@ -91,6 +92,7 @@ class PostAdmin(ModelAdmin):
@admin.register(PostCategory, site=project_admin_site)
class PostCategoryAdmin(ModelAdmin):
list_display = ('name', 'slug', 'language', 'created_at')
list_display_links = ('name', 'slug')
list_filter = ('language',)
search_fields = ('name', 'description')
prepopulated_fields = {'slug': ('name',)}
@ -99,6 +101,7 @@ class PostCategoryAdmin(ModelAdmin):
@admin.register(PostTag, site=project_admin_site)
class PostTagAdmin(ModelAdmin):
list_display = ('name', 'slug')
list_display_links = ('name', 'slug')
search_fields = ('name',)
prepopulated_fields = {'slug': ('name',)}
@ -106,6 +109,7 @@ class PostTagAdmin(ModelAdmin):
@admin.register(PostComment, site=project_admin_site)
class PostCommentAdmin(ModelAdmin):
list_display = ('author_display', 'post', 'status_badge', 'created_at')
list_display_links = ('author_display', 'post')
list_filter = ('status', 'created_at')
search_fields = ('author_name', 'author_email', 'content', 'post__title')
actions = ['approve_comments', 'mark_as_spam']
@ -136,6 +140,7 @@ class PostCommentAdmin(ModelAdmin):
@admin.register(PostLike, site=project_admin_site)
class PostLikeAdmin(ModelAdmin):
list_display = ('user', 'post', 'created_at')
list_display_links = ('user', 'post')
search_fields = ('user__email', 'user__fullname', 'post__title')
readonly_fields = ('created_at',)
@ -143,6 +148,7 @@ class PostLikeAdmin(ModelAdmin):
@admin.register(MediaAsset, site=project_admin_site)
class MediaAssetAdmin(ModelAdmin):
list_display = ('title', 'media_type', 'file_size', 'uploaded_by', 'created_at')
list_display_links = ('title', 'uploaded_by')
list_filter = ('media_type', 'created_at')
search_fields = ('title', 'uploaded_by__email')
readonly_fields = ('created_at',)

1
apps/diplomacy/admin.py

@ -6,6 +6,7 @@ from apps.diplomacy.models import RepresentativeReport
@admin.register(RepresentativeReport)
class RepresentativeReportAdmin(ModelAdmin):
list_display = ('title', 'representative', 'region_hub', 'period', 'centers_visited_count', 'status', 'reviewed_by', 'created_at')
list_display_links = ('title', 'representative', 'region_hub')
list_filter = ('status', 'region_hub', 'created_at')
search_fields = ('title', 'activities_summary', 'representative__email', 'representative__fullname', 'region_hub')
readonly_fields = ('created_at', 'updated_at')

2
apps/donations/admin.py

@ -13,6 +13,7 @@ class DonationTransactionInline(TabularInline):
@admin.register(DonationCampaign)
class DonationCampaignAdmin(ModelAdmin):
list_display = ('title', 'category', 'priority', 'target_amount', 'raised_amount', 'currency', 'is_active', 'is_featured', 'deadline')
list_display_links = ('title', 'category')
list_filter = ('category', 'priority', 'is_active', 'is_featured')
search_fields = ('title', 'description', 'organization_name', 'institution__name')
prepopulated_fields = {'slug': ('title',)}
@ -22,6 +23,7 @@ class DonationCampaignAdmin(ModelAdmin):
@admin.register(DonationTransaction)
class DonationTransactionAdmin(ModelAdmin):
list_display = ('transaction_reference', 'campaign', 'donor_name', 'amount', 'currency', 'payment_status', 'created_at')
list_display_links = ('transaction_reference', 'campaign', 'donor_name')
list_filter = ('payment_status', 'currency', 'created_at')
search_fields = ('transaction_reference', 'donor_name', 'donor_email', 'campaign__title')
readonly_fields = ('transaction_reference', 'created_at')

2
apps/dynamic_forms/admin.py

@ -13,6 +13,7 @@ class FormResponseInline(TabularInline):
@admin.register(FormDefinition)
class FormDefinitionAdmin(ModelAdmin):
list_display = ('title', 'slug', 'is_active', 'is_public', 'target_role', 'created_at')
list_display_links = ('title', 'slug')
list_filter = ('is_active', 'is_public', 'created_at')
search_fields = ('title', 'description', 'slug')
prepopulated_fields = {'slug': ('title',)}
@ -22,6 +23,7 @@ class FormDefinitionAdmin(ModelAdmin):
@admin.register(FormResponse)
class FormResponseAdmin(ModelAdmin):
list_display = ('form', 'user', 'workflow_status', 'reviewed_by', 'created_at')
list_display_links = ('form', 'user', 'workflow_status')
list_filter = ('workflow_status', 'form', 'created_at')
search_fields = ('form__title', 'user__email', 'user__fullname', 'admin_notes')
readonly_fields = ('created_at', 'updated_at')

2
apps/events/admin.py

@ -24,6 +24,7 @@ class EventAdmin(ModelAdmin):
'is_active',
'is_featured',
)
list_display_links = ('title', 'organizer', 'display_category')
list_filter = ('category', 'location_type', 'is_active', 'is_featured', 'language', 'event_date')
search_fields = ('title', 'description', 'speaker_name', 'organizer__name', 'venue_address')
prepopulated_fields = {'slug': ('title',)}
@ -71,6 +72,7 @@ class EventAdmin(ModelAdmin):
@admin.register(EventRegistration)
class EventRegistrationAdmin(ModelAdmin):
list_display = ('event', 'user', 'display_status', 'registered_at')
list_display_links = ('event', 'user', 'display_status')
list_filter = ('status', 'registered_at')
search_fields = ('event__title', 'user__email', 'user__fullname', 'notes')

7
apps/lms/admin.py

@ -32,6 +32,7 @@ class QuizQuestionInline(StackedInline):
@admin.register(Course)
class CourseAdmin(ModelAdmin):
list_display = ('title', 'category', 'language', 'level', 'instructor_name', 'is_published', 'is_featured', 'created_at')
list_display_links = ('title', 'category', 'instructor_name')
list_filter = ('category', 'language', 'level', 'is_published', 'is_featured')
search_fields = ('title', 'description', 'instructor_name', 'organization_name')
prepopulated_fields = {'slug': ('title',)}
@ -41,6 +42,7 @@ class CourseAdmin(ModelAdmin):
@admin.register(CourseModule)
class CourseModuleAdmin(ModelAdmin):
list_display = ('title', 'course', 'order', 'created_at')
list_display_links = ('title', 'course')
list_filter = ('course',)
search_fields = ('title', 'description', 'course__title')
inlines = [LessonInline]
@ -49,6 +51,7 @@ class CourseModuleAdmin(ModelAdmin):
@admin.register(Lesson)
class LessonAdmin(ModelAdmin):
list_display = ('title', 'module', 'lesson_type', 'duration_minutes', 'order', 'is_preview')
list_display_links = ('title', 'module')
list_filter = ('lesson_type', 'is_preview', 'module__course')
search_fields = ('title', 'content', 'module__title', 'module__course__title')
inlines = [QuizQuestionInline]
@ -57,6 +60,7 @@ class LessonAdmin(ModelAdmin):
@admin.register(QuizQuestion)
class QuizQuestionAdmin(ModelAdmin):
list_display = ('question_text', 'lesson', 'correct_option_index', 'order')
list_display_links = ('question_text', 'lesson')
list_filter = ('lesson__module__course',)
search_fields = ('question_text', 'explanation', 'lesson__title')
@ -64,6 +68,7 @@ class QuizQuestionAdmin(ModelAdmin):
@admin.register(CourseEnrollment)
class CourseEnrollmentAdmin(ModelAdmin):
list_display = ('user', 'course', 'progress_percentage', 'enrolled_at', 'completed_at')
list_display_links = ('user', 'course')
list_filter = ('course', 'progress_percentage')
search_fields = ('user__email', 'user__fullname', 'course__title')
@ -71,6 +76,7 @@ class CourseEnrollmentAdmin(ModelAdmin):
@admin.register(LessonProgress)
class LessonProgressAdmin(ModelAdmin):
list_display = ('user', 'lesson', 'is_completed', 'quiz_score', 'completed_at')
list_display_links = ('user', 'lesson')
list_filter = ('is_completed', 'lesson__module__course')
search_fields = ('user__email', 'user__fullname', 'lesson__title')
@ -78,6 +84,7 @@ class LessonProgressAdmin(ModelAdmin):
@admin.register(DigitalCertificate)
class DigitalCertificateAdmin(ModelAdmin):
list_display = ('certificate_code', 'user', 'course', 'issue_date', 'final_score', 'is_revoked')
list_display_links = ('certificate_code', 'user', 'course')
list_filter = ('is_revoked', 'issue_date', 'course')
search_fields = ('certificate_code', 'user__email', 'user__fullname', 'course__title')
readonly_fields = ('uuid', 'created_at')

1
apps/meetings/admin.py

@ -17,6 +17,7 @@ class MeetingRequestAdmin(ModelAdmin):
'created_by',
'created_at',
)
list_display_links = ('title', 'requester_institution', 'recipient_institution', 'created_by')
list_filter = ('status', 'meeting_date', 'requester_institution', 'recipient_institution')
search_fields = (
'title',

5
apps/profiles/admin.py

@ -46,6 +46,7 @@ class InstitutionAdmin(ModelAdmin):
'is_active',
'created_at',
)
list_display_links = ('name', 'type_badge', 'city', 'country')
list_filter = ('type', 'verification_status', 'is_featured', 'is_active', 'country')
search_fields = ('name', 'city', 'country', 'description', 'email', 'phone')
prepopulated_fields = {'slug': ('name',)}
@ -107,6 +108,7 @@ class InstitutionAdmin(ModelAdmin):
@admin.register(InstitutionMember, site=project_admin_site)
class InstitutionMemberAdmin(ModelAdmin):
list_display = ('user', 'institution', 'role_badge', 'title', 'joined_at')
list_display_links = ('user', 'institution', 'title')
list_filter = ('role', 'joined_at')
search_fields = ('user__email', 'user__fullname', 'institution__name', 'title')
readonly_fields = ('joined_at',)
@ -123,6 +125,7 @@ class InstitutionMemberAdmin(ModelAdmin):
@admin.register(InstitutionTimeline, site=project_admin_site)
class InstitutionTimelineAdmin(ModelAdmin):
list_display = ('institution', 'year', 'title', 'order', 'created_at')
list_display_links = ('institution', 'title', 'year')
search_fields = ('institution__name', 'title', 'description', 'year')
list_filter = ('created_at',)
@ -130,6 +133,7 @@ class InstitutionTimelineAdmin(ModelAdmin):
@admin.register(InstitutionMedia, site=project_admin_site)
class InstitutionMediaAdmin(ModelAdmin):
list_display = ('institution', 'title', 'media_type', 'uploaded_at')
list_display_links = ('institution', 'title')
list_filter = ('media_type', 'uploaded_at')
search_fields = ('institution__name', 'title', 'caption')
@ -137,5 +141,6 @@ class InstitutionMediaAdmin(ModelAdmin):
@admin.register(InstitutionFollower, site=project_admin_site)
class InstitutionFollowerAdmin(ModelAdmin):
list_display = ('user', 'institution', 'created_at')
list_display_links = ('user', 'institution')
search_fields = ('user__email', 'user__fullname', 'institution__name')
readonly_fields = ('created_at',)

4
apps/projects/admin.py

@ -38,6 +38,7 @@ class ProjectAdmin(ModelAdmin):
'start_date',
'estimated_end_date',
)
list_display_links = ('title', 'owner_institution', 'category')
list_filter = ('status', 'category', 'owner_institution')
search_fields = ('title', 'description', 'owner_institution__name')
prepopulated_fields = {'slug': ('title',)}
@ -87,6 +88,7 @@ class ProjectAdmin(ModelAdmin):
@admin.register(KanbanColumn, site=project_admin_site)
class KanbanColumnAdmin(ModelAdmin):
list_display = ('name', 'project', 'code', 'order', 'color')
list_display_links = ('name', 'project', 'code')
list_filter = ('project',)
search_fields = ('name', 'project__title')
@ -94,6 +96,7 @@ class KanbanColumnAdmin(ModelAdmin):
@admin.register(KanbanTask, site=project_admin_site)
class KanbanTaskAdmin(ModelAdmin):
list_display = ('title', 'project', 'column', 'priority_badge', 'assignee', 'due_date', 'order')
list_display_links = ('title', 'project', 'column', 'assignee')
list_filter = ('priority', 'project', 'column')
search_fields = ('title', 'description', 'project__title', 'assignee__fullname')
@ -110,6 +113,7 @@ class KanbanTaskAdmin(ModelAdmin):
@admin.register(ProjectDocument, site=project_admin_site)
class ProjectDocumentAdmin(ModelAdmin):
list_display = ('title', 'project', 'doc_type', 'uploaded_by', 'uploaded_at')
list_display_links = ('title', 'project')
list_filter = ('doc_type', 'uploaded_at')
search_fields = ('title', 'project__title')
readonly_fields = ('uploaded_at',)

2
apps/tickets/admin.py

@ -13,6 +13,7 @@ class TicketMessageInline(TabularInline):
@admin.register(Ticket)
class TicketAdmin(ModelAdmin):
list_display = ('tracking_code', 'subject', 'category', 'priority', 'status', 'user', 'institution', 'satisfaction_rating', 'created_at')
list_display_links = ('tracking_code', 'subject', 'user', 'institution')
list_filter = ('status', 'priority', 'category', 'created_at')
search_fields = ('tracking_code', 'subject', 'user__email', 'user__fullname', 'institution__name')
readonly_fields = ('tracking_code', 'created_at', 'updated_at', 'closed_at')
@ -22,6 +23,7 @@ class TicketAdmin(ModelAdmin):
@admin.register(TicketMessage)
class TicketMessageAdmin(ModelAdmin):
list_display = ('ticket', 'sender', 'is_official_response', 'department_label', 'created_at')
list_display_links = ('ticket', 'sender')
list_filter = ('is_official_response', 'created_at')
search_fields = ('ticket__tracking_code', 'content', 'sender__email', 'sender__fullname')
readonly_fields = ('created_at',)

4
config/settings/base.py

@ -348,6 +348,10 @@ UNFOLD = {
"STYLES": [
lambda request: static("css/styles.css"),
],
"SCRIPTS": [
lambda request: static("js/admin_row_click.js"),
],
"TABS": [
{
"page": "accounts",

200
templates/admin/base_site.html

@ -7,7 +7,207 @@
{% endblock %}
{% block extrahead %}
{{ block.super }}
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}">
<script defer src="{% static 'js/admin_row_click.js' %}"></script>
<style>
/* Clickable admin changelist rows */
#result_list tbody tr {
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
#result_list tbody tr:hover {
background-color: rgba(99, 102, 241, 0.05) !important;
}
#result_list tbody tr input,
#result_list tbody tr button,
#result_list tbody tr select,
#result_list tbody tr label,
#result_list tbody tr .actions-row {
cursor: default;
}
#result_list tbody tr .action-checkbox input {
cursor: pointer;
}
/* Clean and spacious filter_horizontal (dual listbox) layout */
.selector {
width: 100% !important;
max-width: 100% !important;
display: flex !important;
flex-direction: row !important;
align-items: stretch !important;
gap: 1.25rem !important;
margin-top: 0.5rem !important;
margin-bottom: 1.5rem !important;
}
.selector-available,
.selector-chosen {
flex: 1 1 0% !important;
min-width: 220px !important;
width: auto !important;
max-width: none !important;
display: flex !important;
flex-direction: column !important;
border-radius: 0.5rem !important;
border: 1px solid rgba(203, 213, 225, 0.8) !important;
background-color: #f8fafc !important;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05) !important;
overflow: hidden !important;
}
.dark .selector-available,
.dark .selector-chosen {
border-color: rgba(51, 65, 85, 0.8) !important;
background-color: #0f172a !important;
}
.selector-available h2,
.selector-chosen h2 {
font-size: 0.875rem !important;
font-weight: 600 !important;
padding: 0.625rem 0.875rem !important;
margin: 0 !important;
border-bottom: 1px solid rgba(203, 213, 225, 0.6) !important;
background-color: rgba(241, 245, 249, 0.6) !important;
}
.dark .selector-available h2,
.dark .selector-chosen h2 {
border-color: rgba(51, 65, 85, 0.6) !important;
background-color: rgba(30, 41, 59, 0.5) !important;
}
.selector-filter input {
width: calc(100% - 1rem) !important;
margin: 0.5rem !important;
padding: 0.4rem 0.75rem !important;
font-size: 0.8125rem !important;
border: 1px solid rgba(203, 213, 225, 0.8) !important;
border-radius: 0.375rem !important;
background-color: #ffffff !important;
}
.dark .selector-filter input {
border-color: rgba(51, 65, 85, 0.8) !important;
background-color: #1e293b !important;
color: #f8fafc !important;
}
.selector select {
flex: 1 1 auto !important;
min-height: 220px !important;
max-height: 320px !important;
height: auto !important;
width: 100% !important;
padding: 0.5rem !important;
border: none !important;
outline: none !important;
background: transparent !important;
}
.selector select option {
padding: 0.35rem 0.5rem !important;
border-radius: 0.25rem !important;
margin-bottom: 1px !important;
}
.selector-chooser {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
gap: 0.5rem !important;
margin: 0 !important;
padding: 0 0.5rem !important;
width: 3.5rem !important;
flex-shrink: 0 !important;
}
.selector-chooser li {
list-style: none !important;
margin: 0 !important;
padding: 0 !important;
}
.selector-chooser a {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 2.25rem !important;
height: 2.25rem !important;
border-radius: 0.375rem !important;
background-color: #f1f5f9 !important;
border: 1px solid #cbd5e1 !important;
color: #334155 !important;
cursor: pointer !important;
transition: all 0.15s ease-in-out !important;
}
.selector-chooser a:hover {
background-color: #6366f1 !important;
border-color: #6366f1 !important;
color: #ffffff !important;
}
.dark .selector-chooser a {
background-color: #1e293b !important;
border-color: #334155 !important;
color: #cbd5e1 !important;
}
.dark .selector-chooser a:hover {
background-color: #6366f1 !important;
border-color: #6366f1 !important;
color: #ffffff !important;
}
.selector-chooseall,
.selector-clearall {
display: block !important;
padding: 0.4rem !important;
font-size: 0.75rem !important;
font-weight: 500 !important;
text-align: center !important;
border-top: 1px solid rgba(203, 213, 225, 0.6) !important;
}
.dark .selector-chooseall,
.dark .selector-clearall {
border-color: rgba(51, 65, 85, 0.6) !important;
}
@media (max-width: 768px) {
.selector {
flex-direction: column !important;
}
.selector-chooser {
flex-direction: row !important;
width: 100% !important;
padding: 0.5rem 0 !important;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
const resultTable = document.getElementById('result_list');
if (!resultTable) return;
resultTable.addEventListener('click', function (e) {
const target = e.target;
// Ignore interactive controls like checkboxes, action selects, buttons, and row expanders
if (target.closest('input, button, select, textarea, label, [data-action], .actions-row, [data-label="Expand row"]')) {
return;
}
// If user clicks a standard hyperlink, allow native browser navigation
const clickedLink = target.closest('a');
if (clickedLink && clickedLink.getAttribute('href') && clickedLink.getAttribute('href') !== '#') {
return;
}
const tr = target.closest('tbody tr');
if (!tr) return;
// Find the change/details link in this row
const changeLink = tr.querySelector('a[href*="/change/"], th a, td a.text-font-important-light, td a[href]');
if (changeLink && changeLink.href) {
if (e.ctrlKey || e.metaKey) {
window.open(changeLink.href, '_blank');
} else {
window.location.href = changeLink.href;
}
}
});
});
</script>
{% if plausible_domain %}
<script defer data-domain="{{ plausible_domain }}" src="https://plausible.io/js/script.js"></script>
{% endif %}
{% endblock %}

25
utils/admin.py

@ -27,6 +27,31 @@ def slugify(value):
# Unfold Imports
from unfold.sites import UnfoldAdminSite
from unfold.admin import ModelAdmin
# --- Enhance ModelAdmin to ensure clicking on items in changelists always navigates to change view ---
_original_get_list_display_links = ModelAdmin.get_list_display_links
def _smart_get_list_display_links(self, request, list_display):
# If list_display_links is explicitly set (or explicitly set to None), respect that choice
if self.list_display_links or self.list_display_links is None:
return self.list_display_links
# If not set, rather than only list_display[0] which might be empty or a non-text field,
# include all candidate primary identifier fields (excluding action checkboxes and editable fields)
editable = getattr(self, 'list_editable', ()) or ()
candidates = [
f for f in list_display
if f != 'action_checkbox' and f not in editable
]
if not candidates:
return (list_display[0],) if list_display else ()
# Return primary candidate columns (up to first 4) so clicking on any main field navigates to details
return tuple(candidates[:4])
ModelAdmin.get_list_display_links = _smart_get_list_display_links
# ---------------------------------------------------------

Loading…
Cancel
Save