From ce9702ae6e4cfbbe120a338cb45cb1dd96877e7a Mon Sep 17 00:00:00 2001 From: lsabor Date: Sat, 22 Nov 2025 07:54:23 -0800 Subject: [PATCH 1/4] add post and project fields to Bulletin --- .../0007_bulletin_post_bulletin_project.py | 36 +++++++++++++++++++ misc/models.py | 22 +++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 misc/migrations/0007_bulletin_post_bulletin_project.py diff --git a/misc/migrations/0007_bulletin_post_bulletin_project.py b/misc/migrations/0007_bulletin_post_bulletin_project.py new file mode 100644 index 0000000000..4ca1ec9c2b --- /dev/null +++ b/misc/migrations/0007_bulletin_post_bulletin_project.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.13 on 2025-11-22 15:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("misc", "0006_whitelistuser_notes_and_more"), + ("posts", "0025_post_actual_resolve_time"), + ("projects", "0021_projectindex_project_index_projectindexpost"), + ] + + operations = [ + migrations.AddField( + model_name="bulletin", + name="post", + field=models.ForeignKey( + help_text="Optional. If set, places this Bulletin only on this post's page.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="posts.post", + ), + ), + migrations.AddField( + model_name="bulletin", + name="project", + field=models.ForeignKey( + help_text="Optional. If set, places this Bulletin only on this project's page.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="projects.project", + ), + ), + ] diff --git a/misc/models.py b/misc/models.py index ac873a487e..6b60126e4e 100644 --- a/misc/models.py +++ b/misc/models.py @@ -43,8 +43,28 @@ class Bulletin(TimeStampedModel): bulletin_end = models.DateTimeField() text = models.TextField() + post = models.ForeignKey( + Post, + null=True, + db_index=True, + on_delete=models.CASCADE, + help_text="""Optional. If set, places this Bulletin only on this post's page.""", + ) + project = models.ForeignKey( + Project, + null=True, + db_index=True, + on_delete=models.CASCADE, + help_text="""Optional. If set, places this Bulletin only on this project's page.""", + ) + def __str__(self): - return self.text[:150] + "..." if len(self.text) > 150 else self.text + text = self.text + if self.post: + text = (self.post.short_title or self.post.title) + ": " + text + elif self.project: + text = self.project.name + ": " + text + return text[:150] + "..." if len(text) > 150 else text class BulletinViewedBy(TimeStampedModel): From 7be3982a7a282464a4a08353d03286ec5c8a3d7d Mon Sep 17 00:00:00 2001 From: lsabor Date: Sat, 22 Nov 2025 08:07:27 -0800 Subject: [PATCH 2/4] update admin and add blank=true --- misc/admin.py | 8 +++++++- misc/migrations/0007_bulletin_post_bulletin_project.py | 4 +++- misc/models.py | 6 ++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/misc/admin.py b/misc/admin.py index 450671bbe5..345b704987 100644 --- a/misc/admin.py +++ b/misc/admin.py @@ -1,3 +1,4 @@ +from admin_auto_filters.filters import AutocompleteFilterFactory from django import forms from django.contrib import admin from django.core.exceptions import ValidationError @@ -8,7 +9,12 @@ @admin.register(Bulletin) class BulletinAdmin(admin.ModelAdmin): list_display = ["__str__", "bulletin_start", "bulletin_end"] - search_fields = ["bulletin_start", "bulletin_end", "text"] + search_fields = ["post", "project", "bulletin_start", "bulletin_end", "text"] + list_filter = [ + AutocompleteFilterFactory("Post", "post"), + AutocompleteFilterFactory("Project", "project"), + ] + autocomplete_fields = ["post", "project"] class SidebarItemAdminForm(forms.ModelForm): diff --git a/misc/migrations/0007_bulletin_post_bulletin_project.py b/misc/migrations/0007_bulletin_post_bulletin_project.py index 4ca1ec9c2b..1d9736fc6c 100644 --- a/misc/migrations/0007_bulletin_post_bulletin_project.py +++ b/misc/migrations/0007_bulletin_post_bulletin_project.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.13 on 2025-11-22 15:53 +# Generated by Django 5.1.13 on 2025-11-22 16:04 import django.db.models.deletion from django.db import migrations, models @@ -17,6 +17,7 @@ class Migration(migrations.Migration): model_name="bulletin", name="post", field=models.ForeignKey( + blank=True, help_text="Optional. If set, places this Bulletin only on this post's page.", null=True, on_delete=django.db.models.deletion.CASCADE, @@ -27,6 +28,7 @@ class Migration(migrations.Migration): model_name="bulletin", name="project", field=models.ForeignKey( + blank=True, help_text="Optional. If set, places this Bulletin only on this project's page.", null=True, on_delete=django.db.models.deletion.CASCADE, diff --git a/misc/models.py b/misc/models.py index 6b60126e4e..8bf1718bb9 100644 --- a/misc/models.py +++ b/misc/models.py @@ -46,6 +46,7 @@ class Bulletin(TimeStampedModel): post = models.ForeignKey( Post, null=True, + blank=True, db_index=True, on_delete=models.CASCADE, help_text="""Optional. If set, places this Bulletin only on this post's page.""", @@ -53,6 +54,7 @@ class Bulletin(TimeStampedModel): project = models.ForeignKey( Project, null=True, + blank=True, db_index=True, on_delete=models.CASCADE, help_text="""Optional. If set, places this Bulletin only on this project's page.""", @@ -61,9 +63,9 @@ class Bulletin(TimeStampedModel): def __str__(self): text = self.text if self.post: - text = (self.post.short_title or self.post.title) + ": " + text + text = (self.post.short_title or self.post.title)[:50] + "... " + text elif self.project: - text = self.project.name + ": " + text + text = self.project.name[:50] + "... " + text return text[:150] + "..." if len(text) > 150 else text From 83733e2cf29d8b2bbb75290a891bcb9488ad5883 Mon Sep 17 00:00:00 2001 From: lsabor Date: Sat, 22 Nov 2025 08:22:02 -0800 Subject: [PATCH 3/4] add post_id and project_id arguments to get_bulletins view --- misc/views.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/misc/views.py b/misc/views.py index ce428de8c3..56fc8ffcb4 100644 --- a/misc/views.py +++ b/misc/views.py @@ -1,10 +1,11 @@ from datetime import datetime -import django +from django.db.models import Q from django.conf import settings from django.core.mail import EmailMessage from django.http import JsonResponse from django.views.decorators.cache import cache_page +from django.utils import timezone from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.exceptions import PermissionDenied @@ -85,12 +86,27 @@ def remove_article_api_view(request, pk): @permission_classes([AllowAny]) def get_bulletins(request): user = request.user + data = request.query_params + post_id = data.get("post_id") + project_id = data.get("project_id") # maybe needs to be slug for simplicity bulletins = Bulletin.objects.filter( - bulletin_start__lte=django.utils.timezone.now(), - bulletin_end__gte=django.utils.timezone.now(), + bulletin_start__lte=timezone.now(), + bulletin_end__gte=timezone.now(), ) + if post_id: + bulletins = bulletins.filter(Q(post_id__isnull=True) | Q(post_id=post_id)) + else: + bulletins = bulletins.filter(post_id__isnull=True) + + if project_id: + bulletins = bulletins.filter( + Q(project_id__isnull=False) | Q(project_id=project_id) + ) + else: + bulletins = bulletins.filter(project_id__isnull=True) + bulletins_viewed_by_user = [] if user and user.is_authenticated: bulletins_viewed_by_user = [ From 6ae14408e2a42c043690cf0d8dce5f07e7d64404 Mon Sep 17 00:00:00 2001 From: lsabor Date: Sat, 22 Nov 2025 08:47:14 -0800 Subject: [PATCH 4/4] get post_id or project_slug from url in order to fetch relevant bulletins. Minor logic fix on backend view --- .../src/app/(main)/components/bulletins.tsx | 20 ++++++++++++++++--- .../src/services/api/misc/misc.shared.ts | 11 ++++++++-- misc/views.py | 6 +++--- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/front_end/src/app/(main)/components/bulletins.tsx b/front_end/src/app/(main)/components/bulletins.tsx index 085dcbc62a..d86f4e7c8f 100644 --- a/front_end/src/app/(main)/components/bulletins.tsx +++ b/front_end/src/app/(main)/components/bulletins.tsx @@ -35,14 +35,28 @@ const Bulletins: FC = () => { ); }, [pathname]); + const bulletinParams = useMemo(() => { + const questionMatch = pathname.match(/^\/questions\/(\d+)(?:\/|$)/); + if (questionMatch) { + return { post_id: Number(questionMatch[1]) }; + } + + const projectMatch = pathname.match(/^\/tournament\/([^/]+)(?:\/|$)/); + if (projectMatch) { + return { project_slug: projectMatch[1] }; + } + + return undefined; + }, [pathname]); + const fetchBulletins = useCallback(async () => { try { - const bulletins = await ClientMiscApi.getBulletins(); - setBulletins(bulletins); + const bulletins = await ClientMiscApi.getBulletins(bulletinParams); + setBulletins(bulletins ?? []); } catch (error) { logError(error); } - }, []); + }, [bulletinParams]); useEffect(() => { if (!shouldHide) { diff --git a/front_end/src/services/api/misc/misc.shared.ts b/front_end/src/services/api/misc/misc.shared.ts index 86f716f394..b28058095d 100644 --- a/front_end/src/services/api/misc/misc.shared.ts +++ b/front_end/src/services/api/misc/misc.shared.ts @@ -1,4 +1,5 @@ import { ApiService } from "@/services/api/api_service"; +import { encodeQueryParams } from "@/utils/navigation"; export type ContactForm = { email: string; @@ -21,14 +22,20 @@ export interface SiteStats { years_of_predictions: number; } +type BulletinParams = { + post_id?: number; + project_slug?: string; +}; + class MiscApi extends ApiService { - async getBulletins() { + async getBulletins(params?: BulletinParams) { + const queryParams = encodeQueryParams(params ?? {}); const resp = await this.get<{ bulletins: { text: string; id: number; }[]; - }>("/get-bulletins/"); + }>(`/get-bulletins/${queryParams}`); return resp?.bulletins; } diff --git a/misc/views.py b/misc/views.py index 56fc8ffcb4..611da007bc 100644 --- a/misc/views.py +++ b/misc/views.py @@ -88,7 +88,7 @@ def get_bulletins(request): user = request.user data = request.query_params post_id = data.get("post_id") - project_id = data.get("project_id") # maybe needs to be slug for simplicity + project_slug = data.get("project_slug") # maybe needs to be slug for simplicity bulletins = Bulletin.objects.filter( bulletin_start__lte=timezone.now(), @@ -100,9 +100,9 @@ def get_bulletins(request): else: bulletins = bulletins.filter(post_id__isnull=True) - if project_id: + if project_slug: bulletins = bulletins.filter( - Q(project_id__isnull=False) | Q(project_id=project_id) + Q(project_id__isnull=True) | Q(project__slug=project_slug) ) else: bulletins = bulletins.filter(project_id__isnull=True)