Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions front_end/src/app/(main)/components/bulletins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react";

import ClientMiscApi from "@/services/api/misc/misc.client";
import { logError } from "@/utils/core/errors";
import { getBulletinParamsFromPathname } from "@/utils/navigation";

import Bulletin from "./bulletin";

Expand Down Expand Up @@ -35,14 +36,19 @@ const Bulletins: FC = () => {
);
}, [pathname]);

const bulletinParams = useMemo(
() => getBulletinParamsFromPathname(pathname),
[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) {
Expand Down
11 changes: 9 additions & 2 deletions front_end/src/services/api/misc/misc.shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApiService } from "@/services/api/api_service";
import { encodeQueryParams } from "@/utils/navigation";

export type ContactForm = {
email: string;
Expand All @@ -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;
}

Expand Down
14 changes: 14 additions & 0 deletions front_end/src/utils/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,17 @@ export function ensureRelativeRedirect(input: string): string {
// Normalize slashes
return "/" + url;
}

export function getBulletinParamsFromPathname(pathname: string) {
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;
}
8 changes: 7 additions & 1 deletion misc/admin.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down
38 changes: 38 additions & 0 deletions misc/migrations/0007_bulletin_post_bulletin_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.1.13 on 2025-11-22 16:04

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(
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,
to="posts.post",
),
),
migrations.AddField(
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,
to="projects.project",
),
),
]
24 changes: 23 additions & 1 deletion misc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,30 @@ class Bulletin(TimeStampedModel):
bulletin_end = models.DateTimeField()
text = models.TextField()

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.""",
)
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.""",
)

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)[:50] + "... " + text
elif self.project:
text = self.project.name[:50] + "... " + text
return text[:150] + "..." if len(text) > 150 else text


class BulletinViewedBy(TimeStampedModel):
Expand Down
22 changes: 19 additions & 3 deletions misc/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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_slug = data.get("project_slug") # 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_slug:
bulletins = bulletins.filter(
Q(project_id__isnull=True) | Q(project__slug=project_slug)
)
else:
bulletins = bulletins.filter(project_id__isnull=True)

bulletins_viewed_by_user = []
if user and user.is_authenticated:
bulletins_viewed_by_user = [
Expand Down