From fa0686793138ca74893ad041033c4575ce69221a Mon Sep 17 00:00:00 2001 From: hlbmtc Date: Tue, 28 Apr 2026 15:31:04 +0200 Subject: [PATCH] Adjust Post & Question admin project filters --- posts/admin.py | 23 ++++++++++++++- projects/admin.py | 72 ++++++++++++++++++++++++---------------------- questions/admin.py | 25 +++++++++++++++- 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/posts/admin.py b/posts/admin.py index 438bd4a1a0..7ff71775a4 100644 --- a/posts/admin.py +++ b/posts/admin.py @@ -1,4 +1,4 @@ -from admin_auto_filters.filters import AutocompleteFilterFactory +from admin_auto_filters.filters import AutocompleteFilterFactory, AutocompleteFilter from django.contrib import admin, messages from django.db.models import QuerySet from django.http import HttpResponse @@ -19,6 +19,26 @@ from utils.models import CustomTranslationAdmin +class DefaultOrSecondaryProjectFilter(AutocompleteFilter): + """Autocomplete filter — `?default_or_secondary_project=` matches posts + where the project is the default_project OR appears in the projects M2M.""" + + title = "Default or Secondary Project" + field_name = "default_project" + parameter_name = "default_or_secondary_project" + use_pk_exact = False + + def queryset(self, request, queryset): + value = self.value() + if not value: + return queryset + try: + project = Project.objects.get(pk=int(value)) + except (Project.DoesNotExist, ValueError): + return queryset.none() + return queryset.filter_projects(project) + + @admin.register(Post) class PostAdmin(CustomTranslationAdmin): list_display = [ @@ -36,6 +56,7 @@ class PostAdmin(CustomTranslationAdmin): "show_on_homepage", AutocompleteFilterFactory("Default Project", "default_project"), AutocompleteFilterFactory("Project", "projects"), + DefaultOrSecondaryProjectFilter, ] autocomplete_fields = [ "author", diff --git a/projects/admin.py b/projects/admin.py index f356b9928c..8a99860259 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -1,4 +1,5 @@ import random +from urllib.parse import urlencode from admin_auto_filters.filters import AutocompleteFilterFactory from django import forms @@ -790,42 +791,34 @@ def view_posts_link(self, obj): def posts_summary(self, obj): if obj.type == Project.ProjectTypes.SITE_MAIN: return None - all_posts = Post.objects.filter( - Q(default_project=obj) | Q(projects=obj) - ).distinct("id") - all_ids = list(all_posts.values_list("id", flat=True)) - all_count = len(all_ids) - - default_posts = Post.objects.filter(default_project=obj) - default_posts_ids = list(default_posts.values_list("id", flat=True)) - default_posts_count = len(default_posts_ids) - - posts = Post.objects.filter(projects=obj).distinct("id") - posts_ids = list(posts.values_list("id", flat=True)) - posts_count = len(posts_ids) - - def make_link(ids, label): - query_string = f"id__in={','.join(map(str, ids)) or '0'}" - url = reverse("admin:posts_post_changelist") + "?" + query_string - return format_html('{}', url, label) - + all_count = Post.objects.filter_projects(obj).count() default_posts_count = Post.objects.filter(default_project=obj).count() posts_count = Post.objects.filter(projects=obj).count() + + def make_link(label, **query_params): + url = reverse("admin:posts_post_changelist") + "?" + urlencode(query_params) + return format_html('{}', url, label) + return format_html_join( format_html("
"), "{}", [ - (make_link(all_ids, f"{all_count} Posts in Project"),), ( make_link( - default_posts_ids, + f"{all_count} Posts in Project", + default_or_secondary_project=obj.id, + ), + ), + ( + make_link( f"{default_posts_count} Default Posts (Determines Permissions)", + default_project=obj.id, ), ), ( make_link( - posts_ids, f"{posts_count} Secondary Posts", + projects=obj.id, ), ), ], @@ -840,46 +833,57 @@ def questions_summary(self, obj): if not leaderboard: return None - all_questions = leaderboard.get_questions().filter(question_weight__gt=0) - all_ids = list(all_questions.values_list("id", flat=True)) - all_count = len(all_ids) + all_count = Question.objects.filter( + post__in=Post.objects.filter_projects(obj) + ).count() + + weighted_questions = leaderboard.get_questions().filter(question_weight__gt=0) + weighted_ids = list(weighted_questions.values_list("id", flat=True)) finalize_time = leaderboard.finalize_time or obj.close_date if finalize_time: - in_leaderboard_qs = all_questions.filter( + in_leaderboard_qs = weighted_questions.filter( Q(resolution_set_time__isnull=True) | Q(resolution_set_time__lte=finalize_time), scheduled_close_time__lte=finalize_time, ) else: - in_leaderboard_qs = all_questions + in_leaderboard_qs = weighted_questions in_leaderboard_ids = list(in_leaderboard_qs.values_list("id", flat=True)) in_leaderboard_count = len(in_leaderboard_ids) - not_in_leaderboard_ids = list(set(all_ids) - set(in_leaderboard_ids)) + not_in_leaderboard_ids = list(set(weighted_ids) - set(in_leaderboard_ids)) not_in_leaderboard_count = len(not_in_leaderboard_ids) - def make_link(ids, label): - query_string = f"id__in={','.join(map(str, ids)) or '0'}" - url = reverse("admin:questions_question_changelist") + "?" + query_string + def make_link(label, **query_params): + url = ( + reverse("admin:questions_question_changelist") + + "?" + + urlencode(query_params) + ) return format_html('{}', url, label) return format_html_join( format_html("
"), "{}", [ - (make_link(all_ids, f"{all_count} Questions in Project"),), ( make_link( - in_leaderboard_ids, + f"{all_count} Questions in Project", + default_or_secondary_project=obj.id, + ), + ), + ( + make_link( f"{in_leaderboard_count} Questions in Primary Leaderboard", + id__in=",".join(map(str, in_leaderboard_ids)) or "0", ), ), ( make_link( - not_in_leaderboard_ids, f"{not_in_leaderboard_count} Questions NOT in Primary Leaderboard", + id__in=",".join(map(str, not_in_leaderboard_ids)) or "0", ), ), ], diff --git a/questions/admin.py b/questions/admin.py index 2e70f1a62c..934031bff8 100644 --- a/questions/admin.py +++ b/questions/admin.py @@ -1,4 +1,4 @@ -from admin_auto_filters.filters import AutocompleteFilterFactory +from admin_auto_filters.filters import AutocompleteFilterFactory, AutocompleteFilter from datetime import datetime, timedelta, timezone as dt_timezone from django import forms @@ -15,6 +15,7 @@ from posts.models import Post from posts.tasks import run_post_generate_history_snapshot +from projects.models import Project from questions.constants import UnsuccessfulResolutionType from questions.models import ( AggregateForecast, @@ -480,6 +481,27 @@ def clean(self): raise forms.ValidationError("Invalid action selected.") +class DefaultOrSecondaryProjectFilter(AutocompleteFilter): + """Autocomplete filter — `?default_or_secondary_project=` matches + questions whose post has the project as default_project OR in the projects M2M.""" + + title = "Default or Secondary Project" + field_name = "default_project" + rel_model = Post + parameter_name = "default_or_secondary_project" + use_pk_exact = False + + def queryset(self, request, queryset): + value = self.value() + if not value: + return queryset + try: + project = Project.objects.get(pk=int(value)) + except (Project.DoesNotExist, ValueError): + return queryset.none() + return queryset.filter(post__in=Post.objects.filter_projects(project)) + + @admin.register(Question) class QuestionAdmin(CustomTranslationAdmin, DynamicArrayMixin): form = QuestionAdminForm @@ -521,6 +543,7 @@ class QuestionAdmin(CustomTranslationAdmin, DynamicArrayMixin): AutocompleteFilterFactory("Author", "post__author"), AutocompleteFilterFactory("Default Project", "post__default_project"), AutocompleteFilterFactory("Project", "post__projects"), + DefaultOrSecondaryProjectFilter, ] autocomplete_fields = ["group"]