diff --git a/apps/account/admin/location.py b/apps/account/admin/location.py index 2b48d05..b25196d 100644 --- a/apps/account/admin/location.py +++ b/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', diff --git a/apps/account/admin/login_history.py b/apps/account/admin/login_history.py index 0c57e2d..2ae576f 100644 --- a/apps/account/admin/login_history.py +++ b/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', diff --git a/apps/account/admin/notification.py b/apps/account/admin/notification.py index cab1fc6..3870ded 100644 --- a/apps/account/admin/notification.py +++ b/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',) diff --git a/apps/account/admin/role.py b/apps/account/admin/role.py index 401c729..aa8036a 100644 --- a/apps/account/admin/role.py +++ b/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',) diff --git a/apps/account/admin/user.py b/apps/account/admin/user.py index 510dfc8..198d569 100644 --- a/apps/account/admin/user.py +++ b/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',) diff --git a/apps/api/admin.py b/apps/api/admin.py index 8dfacf5..fe55e02 100644 --- a/apps/api/admin.py +++ b/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'] diff --git a/apps/chat/admin.py b/apps/chat/admin.py index 198bd02..cd74efc 100644 --- a/apps/chat/admin.py +++ b/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',) diff --git a/apps/cms/admin.py b/apps/cms/admin.py index 896f429..b187c6a 100644 --- a/apps/cms/admin.py +++ b/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',) diff --git a/apps/diplomacy/admin.py b/apps/diplomacy/admin.py index 08c454f..226db2b 100644 --- a/apps/diplomacy/admin.py +++ b/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') diff --git a/apps/donations/admin.py b/apps/donations/admin.py index 80ba094..c2d6e3e 100644 --- a/apps/donations/admin.py +++ b/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') diff --git a/apps/dynamic_forms/admin.py b/apps/dynamic_forms/admin.py index 739c0e9..ab60b50 100644 --- a/apps/dynamic_forms/admin.py +++ b/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') diff --git a/apps/events/admin.py b/apps/events/admin.py index 4452caa..f5368a6 100644 --- a/apps/events/admin.py +++ b/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') diff --git a/apps/lms/admin.py b/apps/lms/admin.py index 6913243..7506bcd 100644 --- a/apps/lms/admin.py +++ b/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') diff --git a/apps/meetings/admin.py b/apps/meetings/admin.py index c55cd83..aefd8eb 100644 --- a/apps/meetings/admin.py +++ b/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', diff --git a/apps/profiles/admin.py b/apps/profiles/admin.py index fc3e862..f748122 100644 --- a/apps/profiles/admin.py +++ b/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',) diff --git a/apps/projects/admin.py b/apps/projects/admin.py index e4f9fdc..db1a577 100644 --- a/apps/projects/admin.py +++ b/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',) diff --git a/apps/tickets/admin.py b/apps/tickets/admin.py index 5dff847..b6e49cd 100644 --- a/apps/tickets/admin.py +++ b/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',) diff --git a/config/settings/base.py b/config/settings/base.py index 6901afa..bac46e9 100644 --- a/config/settings/base.py +++ b/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", diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html index 83e0b87..e49fe9f 100644 --- a/templates/admin/base_site.html +++ b/templates/admin/base_site.html @@ -7,7 +7,207 @@ {% endblock %} {% block extrahead %} + {{ block.super }} + {% load static %} + + + + {% if plausible_domain %} {% endif %} {% endblock %} + diff --git a/utils/admin.py b/utils/admin.py index 62dc3eb..7f00592 100644 --- a/utils/admin.py +++ b/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 + # ---------------------------------------------------------