From 16261018fa629345b1babbb032b9c907a7f5438f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 12:42:40 +0000 Subject: [PATCH] Rename project date fields: close_date -> winners_announced_date, forecasting_end_date -> close_date The previous naming was confusing because the field labelled "Closes" on the front end was named `forecasting_end_date` in the backend, while `close_date` actually referred to the wrap-up / prize payout date displayed as "Winners announced date". This commit aligns the backend with the user-facing language: - close_date (deadline / prizes paid) -> winners_announced_date - forecasting_end_date (last scored question close) -> close_date - Improved help_text on both fields - Renamed move_project_forecasting_end_date -> move_project_close_date - Updated all backend references, frontend types/components, mock data, and i18n placeholder name (English copy fully updated; other locales updated to use the new placeholder so they remain functional pending translation refresh). Includes migration 0024 that swaps the two columns via a temporary name. Refs #4394 Co-authored-by: Sylvain --- front_end/messages/cs.json | 2 +- front_end/messages/en.json | 2 +- front_end/messages/es.json | 2 +- front_end/messages/pt.json | 2 +- front_end/messages/zh-TW.json | 2 +- front_end/messages/zh.json | 2 +- .../components/closed_tournament_timeline.tsx | 4 +- .../tournament/components/dropdown_menu.tsx | 12 ++--- .../components/header_block_info.tsx | 4 +- .../components/tournament_timeline.tsx | 2 +- .../components/tournaments_filter.tsx | 2 +- .../tournaments_grid/live_tournament_card.tsx | 20 +++---- .../tournaments/helpers/index.ts | 2 +- .../tournaments/helpers/tournament_filters.ts | 10 ++-- .../src/app/(main)/aib/minibench/page.tsx | 2 +- .../[id]/components/post_approval_modal.tsx | 26 ++++----- .../posts_feed/feed_tournament_tile.tsx | 6 +-- .../feed_card/binary_group/mock_data.ts | 4 +- .../feed_card/binary_question/mock_data.ts | 8 +-- .../continuous_question/mock_data.ts | 6 +-- .../feed_card/date_group/mock_data.tsx | 4 +- .../feed_card/date_question/mock_data.ts | 14 ++--- .../feed_card/mc_question/mock_data.ts | 4 +- .../feed_card/numeric_group/mock_data.ts | 8 +-- front_end/src/types/projects.ts | 4 +- posts/services/common.py | 4 +- projects/admin.py | 20 +++---- .../0024_rename_project_date_fields.py | 53 +++++++++++++++++++ projects/models.py | 19 ++++--- projects/serializers/common.py | 4 +- projects/services/common.py | 44 +++++++-------- scoring/jobs.py | 2 +- scoring/models.py | 2 +- scoring/utils.py | 12 ++--- 34 files changed, 183 insertions(+), 131 deletions(-) create mode 100644 projects/migrations/0024_rename_project_date_fields.py diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 5a1171d0b2..4abf0029b9 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -1220,7 +1220,7 @@ "communityDisclaimer": "Přispěl komunita. Dozvědět se více", "communityDisclaimerSiteMain": "Tato otázka je také součástí hlavního Metaculus.", "conditionalBranchResolutionAnnulled": "Tato větev byla zrušena, protože nadřazená otázka byla vyřešena .", - "postNotebookMoveDateModalCopy": "Datum ukončení předpovědí turnaje je {tournament_forecasting_end_date} a snažíte se schválit otázku, která se uzavírá {question_close_date}. Chcete přemístit datum ukončení předpovědí turnaje na {question_close_date}?", + "postNotebookMoveDateModalCopy": "Datum ukončení předpovědí turnaje je {tournament_close_date} a snažíte se schválit otázku, která se uzavírá {question_close_date}. Chcete přemístit datum ukončení předpovědí turnaje na {question_close_date}?", "moveDate": "Přemístit datum", "addKeyFactor": "Přidat klíčový faktor", "addKeyFactors": "Přidat klíčové faktory", diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 392a5a4764..296a7ce452 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -1463,7 +1463,7 @@ "communityDisclaimer": "Contributed by the community. Learn more", "conditionalBranchResolutionAnnulled": "This branch was annulled because the parent question resolved .", "communityDisclaimerSiteMain": "This question is also part of main Metaculus.", - "postNotebookMoveDateModalCopy": "The tournament forecasting end date is {tournament_forecasting_end_date} and you are trying to approve a question that closes on {question_close_date}. Do you want to move the tournament forecasting end date to {question_close_date}?", + "postNotebookMoveDateModalCopy": "The tournament close date is {tournament_close_date} and you are trying to approve a question that closes on {question_close_date}. Do you want to move the tournament close date to {question_close_date}?", "moveDate": "Move date", "addKeyFactor": "Add key factor", "addDriver": "Add Driver", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index ac46d8796e..71326d0fc1 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -1219,7 +1219,7 @@ "communityDisclaimer": "Contribuido por la comunidad de . Más información", "communityDisclaimerSiteMain": "Esta pregunta también forma parte de Metaculus principal.", "conditionalBranchResolutionAnnulled": "Esta rama fue anulada porque la pregunta principal se resolvió .", - "postNotebookMoveDateModalCopy": "La fecha de finalización de pronósticos del torneo es {tournament_forecasting_end_date} y estás tratando de aprobar una pregunta que cierra el {question_close_date}. ¿Quieres mover la fecha de finalización de pronósticos del torneo al {question_close_date}?", + "postNotebookMoveDateModalCopy": "La fecha de finalización de pronósticos del torneo es {tournament_close_date} y estás tratando de aprobar una pregunta que cierra el {question_close_date}. ¿Quieres mover la fecha de finalización de pronósticos del torneo al {question_close_date}?", "moveDate": "Mover fecha", "addKeyFactor": "Añadir factor clave", "addKeyFactors": "Añadir factores clave", diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 8895cd7cc4..3b5ae71f04 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -1217,7 +1217,7 @@ "communityDisclaimer": "Contribuído pela comunidade . Saiba mais", "communityDisclaimerSiteMain": "Esta pergunta também faz parte do Metaculus principal.", "conditionalBranchResolutionAnnulled": "Este ramo foi anulado porque a pergunta pai foi resolvida .", - "postNotebookMoveDateModalCopy": "A data de término das previsões do torneio é {tournament_forecasting_end_date} e você está tentando aprovar uma pergunta que fecha em {question_close_date}. Deseja mover a data de término das previsões do torneio para {question_close_date}?", + "postNotebookMoveDateModalCopy": "A data de término das previsões do torneio é {tournament_close_date} e você está tentando aprovar uma pergunta que fecha em {question_close_date}. Deseja mover a data de término das previsões do torneio para {question_close_date}?", "moveDate": "Mover data", "addKeyFactor": "Adicionar fator chave", "addKeyFactors": "Adicionar Fatores Chave", diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 31dc25025c..95aa557da0 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -1185,7 +1185,7 @@ "communityDisclaimer": "由社群提供。了解更多", "conditionalBranchResolutionAnnulled": "此分支已取消,因為父問題解決了。", "communityDisclaimerSiteMain": "這個問題也是 Metaculus 主站的一部分。", - "postNotebookMoveDateModalCopy": "比賽預測的結束日期是{tournament_forecasting_end_date},而你正嘗試批准一個截止日期為{question_close_date}的問題。你想將比賽預測的結束日期移動到{question_close_date}嗎?", + "postNotebookMoveDateModalCopy": "比賽預測的結束日期是{tournament_close_date},而你正嘗試批准一個截止日期為{question_close_date}的問題。你想將比賽預測的結束日期移動到{question_close_date}嗎?", "moveDate": "移動日期", "addKeyFactor": "加入關鍵因素", "addKeyFactors": "加入關鍵因素", diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index a4586eedec..cffcc34901 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -1221,7 +1221,7 @@ "communityDisclaimer": "由社区贡献。了解更多", "communityDisclaimerSiteMain": "这个问题也是 Metaculus 主站的一部分。", "conditionalBranchResolutionAnnulled": "此分支被取消,因为父问题解决。", - "postNotebookMoveDateModalCopy": "比赛预测结束日期为 {tournament_forecasting_end_date},而您正在尝试批准一个关闭日期为 {question_close_date} 的问题。您是否想将比赛预测结束日期移至 {question_close_date}?", + "postNotebookMoveDateModalCopy": "比赛预测结束日期为 {tournament_close_date},而您正在尝试批准一个关闭日期为 {question_close_date} 的问题。您是否想将比赛预测结束日期移至 {question_close_date}?", "moveDate": "移动日期", "addKeyFactor": "添加关键因素", "addKeyFactors": "添加关键因素", diff --git a/front_end/src/app/(main)/(tournaments)/tournament/components/closed_tournament_timeline.tsx b/front_end/src/app/(main)/(tournaments)/tournament/components/closed_tournament_timeline.tsx index 699a4daa58..78c52136de 100644 --- a/front_end/src/app/(main)/(tournaments)/tournament/components/closed_tournament_timeline.tsx +++ b/front_end/src/app/(main)/(tournaments)/tournament/components/closed_tournament_timeline.tsx @@ -30,8 +30,8 @@ const ClosedTournamentTimeline: FC = async ({ latestScheduledResolutionTimestamp = addDays(dateNowTimestamp, 1).getTime(); } - const endDateTimestamp = tournament.close_date - ? new Date(tournament.close_date).getTime() + const endDateTimestamp = tournament.winners_announced_date + ? new Date(tournament.winners_announced_date).getTime() : addWeeks( isAllQuestionsResolved ? latestActualResolutionTimestamp diff --git a/front_end/src/app/(main)/(tournaments)/tournament/components/dropdown_menu.tsx b/front_end/src/app/(main)/(tournaments)/tournament/components/dropdown_menu.tsx index 76c66b1600..6ca96f52a9 100644 --- a/front_end/src/app/(main)/(tournaments)/tournament/components/dropdown_menu.tsx +++ b/front_end/src/app/(main)/(tournaments)/tournament/components/dropdown_menu.tsx @@ -99,8 +99,8 @@ const TournamentStats = ({ tournament }: { tournament: Tournament }) => { const isUpcoming = new Date(tournament.start_date || "").getTime() > Date.now(); const forecastingClosed = - tournament.forecasting_end_date && - new Date(tournament.forecasting_end_date).getTime() < Date.now(); + tournament.close_date && + new Date(tournament.close_date).getTime() < Date.now(); const questionsResolved = !!tournament.timeline.latest_actual_resolve_time; return (
@@ -114,7 +114,7 @@ const TournamentStats = ({ tournament }: { tournament: Tournament }) => {
- {tournament.forecasting_end_date && ( + {tournament.close_date && (
{forecastingClosed @@ -123,7 +123,7 @@ const TournamentStats = ({ tournament }: { tournament: Tournament }) => { : - +
)} @@ -142,13 +142,13 @@ const TournamentStats = ({ tournament }: { tournament: Tournament }) => { )} - {tournament.close_date && ( + {tournament.winners_announced_date && (
{t("winnersAnnounced")}: - +
)} diff --git a/front_end/src/app/(main)/(tournaments)/tournament/components/header_block_info.tsx b/front_end/src/app/(main)/(tournaments)/tournament/components/header_block_info.tsx index d3fbe561ec..45c84435fa 100644 --- a/front_end/src/app/(main)/(tournaments)/tournament/components/header_block_info.tsx +++ b/front_end/src/app/(main)/(tournaments)/tournament/components/header_block_info.tsx @@ -46,10 +46,10 @@ const HeaderBlockInfo: FC = async ({ tournament }) => { text={formatDate(locale, new Date(tournament.start_date))} /> )} - {!isNil(tournament.close_date) && ( + {!isNil(tournament.winners_announced_date) && ( )} diff --git a/front_end/src/app/(main)/(tournaments)/tournament/components/tournament_timeline.tsx b/front_end/src/app/(main)/(tournaments)/tournament/components/tournament_timeline.tsx index 322cbd4837..541382096a 100644 --- a/front_end/src/app/(main)/(tournaments)/tournament/components/tournament_timeline.tsx +++ b/front_end/src/app/(main)/(tournaments)/tournament/components/tournament_timeline.tsx @@ -42,7 +42,7 @@ const TournamentTimeline: FC = async ({ tournament }) => { } = tournament.timeline; const latestScheduledCloseTimestamp = getTimestampFromDateString( - tournament.forecasting_end_date || tournament.close_date + tournament.close_date || tournament.winners_announced_date ); const nowTs = Date.now(); diff --git a/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_filter.tsx b/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_filter.tsx index 17e44298ad..333e11f780 100644 --- a/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_filter.tsx +++ b/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_filter.tsx @@ -26,7 +26,7 @@ const TournamentsFilter: React.FC = () => { }, { label: t("endingSoon"), - value: TournamentsSortBy.CloseDateAsc, + value: TournamentsSortBy.WinnersAnnouncedDateAsc, }, { label: t("newest"), diff --git a/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_grid/live_tournament_card.tsx b/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_grid/live_tournament_card.tsx index 1fe2e753f2..67df5e2763 100644 --- a/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_grid/live_tournament_card.tsx +++ b/front_end/src/app/(main)/(tournaments)/tournaments/components/tournaments_grid/live_tournament_card.tsx @@ -89,8 +89,8 @@ const LiveTournamentCard: React.FC = ({ nowTs={nowTs} timeline={item.timeline ?? null} startDate={item.start_date ?? null} - forecastingEndDate={item.forecasting_end_date ?? null} closeDate={item.close_date ?? null} + winnersAnnouncedDate={item.winners_announced_date ?? null} isOngoing={Boolean(item.is_ongoing)} /> )} @@ -103,19 +103,19 @@ function TournamentTimelineBar({ nowTs, timeline, startDate, - forecastingEndDate, closeDate, + winnersAnnouncedDate, isOngoing, }: { nowTs: number | null; timeline: TournamentTimeline | null; startDate?: string | null; - forecastingEndDate?: string | null; closeDate?: string | null; + winnersAnnouncedDate?: string | null; isOngoing: boolean; }) { const startTs = safeTs(startDate); - const closedTs = safeTs(forecastingEndDate ?? closeDate ?? null); + const closedTs = safeTs(closeDate ?? winnersAnnouncedDate ?? null); if (!startTs || !closedTs) return null; const now = nowTs ?? Date.now(); @@ -136,7 +136,7 @@ function TournamentTimelineBar({ ); @@ -196,12 +196,12 @@ function ClosedMiniBar({ nowTs, isResolved, timeline, - closeDate, + winnersAnnouncedDate, }: { nowTs: number | null; isResolved: boolean; timeline: TournamentTimeline | null; - closeDate: string | null; + winnersAnnouncedDate: string | null; }) { const t = useTranslations(); const label = isResolved @@ -211,7 +211,7 @@ function ClosedMiniBar({ if (nowTs != null) { const resolvedTs = pickResolveTs(nowTs, timeline); - const winnersTs = pickWinnersTs(resolvedTs, closeDate); + const winnersTs = pickWinnersTs(resolvedTs, winnersAnnouncedDate); if (resolvedTs && nowTs >= resolvedTs) progress = 50; if (winnersTs && nowTs >= winnersTs) progress = 100; @@ -286,8 +286,8 @@ function pickResolveTs(nowTs: number, timeline: TournamentTimeline | null) { return (isAllResolved ? actual : null) ?? effectiveScheduled ?? null; } -function pickWinnersTs(resolvedTs: number | null, closeDate: string | null) { - const closeTs = safeTs(closeDate); +function pickWinnersTs(resolvedTs: number | null, winnersAnnouncedDate: string | null) { + const closeTs = safeTs(winnersAnnouncedDate); if (closeTs) return closeTs; return resolvedTs ? resolvedTs + TWO_WEEKS_MS : null; } diff --git a/front_end/src/app/(main)/(tournaments)/tournaments/helpers/index.ts b/front_end/src/app/(main)/(tournaments)/tournaments/helpers/index.ts index c1646f4cc2..7f237ab27f 100644 --- a/front_end/src/app/(main)/(tournaments)/tournaments/helpers/index.ts +++ b/front_end/src/app/(main)/(tournaments)/tournaments/helpers/index.ts @@ -10,7 +10,7 @@ import { import { TournamentsSection } from "../types"; const archiveEndTs = (t: TournamentPreview) => - [t.forecasting_end_date, t.close_date, t.start_date] + [t.close_date, t.winners_announced_date, t.start_date] .map((s) => (s ? toDate(s.trim(), { timeZone: "UTC" }) : null)) .find((d) => d && isValid(d)) ?.getTime() ?? 0; diff --git a/front_end/src/app/(main)/(tournaments)/tournaments/helpers/tournament_filters.ts b/front_end/src/app/(main)/(tournaments)/tournaments/helpers/tournament_filters.ts index 5eac8b0553..337688af89 100644 --- a/front_end/src/app/(main)/(tournaments)/tournaments/helpers/tournament_filters.ts +++ b/front_end/src/app/(main)/(tournaments)/tournaments/helpers/tournament_filters.ts @@ -56,10 +56,10 @@ export function filterTournaments( case TournamentsSortBy.PrizePoolDesc: return Number(b.prize_pool) - Number(a.prize_pool); - case TournamentsSortBy.CloseDateAsc: + case TournamentsSortBy.WinnersAnnouncedDateAsc: return differenceInMilliseconds( - new Date(a.close_date ?? 0), - new Date(b.close_date ?? 0) + new Date(a.winners_announced_date ?? 0), + new Date(b.winners_announced_date ?? 0) ); case TournamentsSortBy.StartDateDesc: @@ -97,7 +97,7 @@ const statusRank = (t: TournamentPreview, nowTs: number): number => { const hasQuestions = t.questions_count > 0; const allClosed = hasQuestions && !!t.timeline?.all_questions_closed; const allResolved = hasQuestions && !!t.timeline?.all_questions_resolved; - const forecastEndTs = safeTs(t.forecasting_end_date); + const forecastEndTs = safeTs(t.close_date); const forecastEnded = forecastEndTs != null && nowTs >= forecastEndTs; const forecastOpen = forecastEndTs != null && !forecastEnded; @@ -112,7 +112,7 @@ const orderValue = (t: TournamentPreview): number => const durationPctPassed = (t: TournamentPreview, nowTs: number): number => { const startTs = safeTs(t.start_date); - const endTs = safeTs(t.forecasting_end_date ?? t.close_date); + const endTs = safeTs(t.close_date ?? t.winners_announced_date); if (startTs == null || endTs == null || endTs <= startTs) { return Number.POSITIVE_INFINITY; } diff --git a/front_end/src/app/(main)/aib/minibench/page.tsx b/front_end/src/app/(main)/aib/minibench/page.tsx index bd8755be9d..6b9f356b73 100644 --- a/front_end/src/app/(main)/aib/minibench/page.tsx +++ b/front_end/src/app/(main)/aib/minibench/page.tsx @@ -86,7 +86,7 @@ export default async function MiniBenchPage() { name={tournament.name} questionsCount={tournament.questions_count} closeDate={ - tournament.forecasting_end_date || tournament.close_date + tournament.close_date || tournament.winners_announced_date } showCloseDate={ tournament.type !== TournamentType.QuestionSeries diff --git a/front_end/src/app/(main)/questions/[id]/components/post_approval_modal.tsx b/front_end/src/app/(main)/questions/[id]/components/post_approval_modal.tsx index 51fda266b1..47b6f64e51 100644 --- a/front_end/src/app/(main)/questions/[id]/components/post_approval_modal.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/post_approval_modal.tsx @@ -27,7 +27,7 @@ const PostApprovalModal: FC<{ const [isLoading, setIsLoading] = useState(false); const [submitErrors, setSubmitErrors] = useState(); const [activeModal, setActiveModal] = useState< - "approvePost" | "confirmForecastingEndDate" + "approvePost" | "confirmCloseDate" >(); const default_project = post.projects?.default_project; @@ -161,23 +161,23 @@ const PostApprovalModal: FC<{ const handleApprovePostSubmit = useCallback(async () => { if (!default_project) return; - const { forecasting_end_date, close_date } = default_project; + const { close_date, winners_announced_date } = default_project; const { scheduled_close_time, scheduled_resolve_time } = approvalData; - // Checks whether given post potentially affects Project.forecasting_end_date value + // Checks whether given post potentially affects Project.close_date value if ( // Show popup for single questions only post.question && // Only eligible for Tournaments default_project.type == TournamentType.Tournament && - forecasting_end_date && close_date && + winners_announced_date && scheduled_close_time && scheduled_resolve_time && - isAfter(new Date(scheduled_close_time), new Date(forecasting_end_date)) && - !isAfter(new Date(scheduled_resolve_time), new Date(close_date)) + isAfter(new Date(scheduled_close_time), new Date(close_date)) && + !isAfter(new Date(scheduled_resolve_time), new Date(winners_announced_date)) ) { - setActiveModal("confirmForecastingEndDate"); + setActiveModal("confirmCloseDate"); } else { await handleApprove(); } @@ -349,7 +349,7 @@ const PostApprovalModal: FC<{ {" "} { setIsOpen(false); @@ -358,13 +358,9 @@ const PostApprovalModal: FC<{

{t.rich("postNotebookMoveDateModalCopy", { - tournament_forecasting_end_date: - default_project?.forecasting_end_date - ? formatDate( - locale, - new Date(default_project.forecasting_end_date) - ) - : "", + tournament_close_date: default_project?.close_date + ? formatDate(locale, new Date(default_project.close_date)) + : "", question_close_date: approvalData.scheduled_close_time ? formatDate( locale, diff --git a/front_end/src/components/posts_feed/feed_tournament_tile.tsx b/front_end/src/components/posts_feed/feed_tournament_tile.tsx index fc1e275bb7..d34264233c 100644 --- a/front_end/src/components/posts_feed/feed_tournament_tile.tsx +++ b/front_end/src/components/posts_feed/feed_tournament_tile.tsx @@ -39,8 +39,8 @@ const FeedTournamentTile: FC = ({ tile, feedPage }) => { const rawCloseTime = rule === FeedTileRule.ALL_QUESTIONS_RESOLVED && project_resolution_date ? project_resolution_date - : project.close_date ?? - project.forecasting_end_date ?? + : project.winners_announced_date ?? + project.close_date ?? project.start_date; const rawCloseTs = safeTs(rawCloseTime) ?? Date.now() + 1000; const scheduled_close_time = new Date( @@ -158,7 +158,7 @@ function getStatusLabel( ): ReactNode | null { const now = Date.now(); const startTs = safeTs(project.start_date); - const closeTs = safeTs(project.close_date); + const closeTs = safeTs(project.winners_announced_date); const resolveTs = safeTs(project_resolution_date); const strong = (chunks: ReactNode) => {chunks}; diff --git a/front_end/src/stories/feed_card/binary_group/mock_data.ts b/front_end/src/stories/feed_card/binary_group/mock_data.ts index 1a3a8c881e..5d445654bf 100644 --- a/front_end/src/stories/feed_card/binary_group/mock_data.ts +++ b/front_end/src/stories/feed_card/binary_group/mock_data.ts @@ -61,8 +61,8 @@ export const getMockData = (isResolved: boolean, isFanChart?: boolean) => { header_image: null, prize_pool: null, start_date: "2025-06-18T13:00:10Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: null, @@ -82,8 +82,8 @@ export const getMockData = (isResolved: boolean, isFanChart?: boolean) => { header_image: null, prize_pool: null, start_date: "2025-06-18T13:00:10Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: null, diff --git a/front_end/src/stories/feed_card/binary_question/mock_data.ts b/front_end/src/stories/feed_card/binary_question/mock_data.ts index 4970bac01d..7e3419e002 100644 --- a/front_end/src/stories/feed_card/binary_question/mock_data.ts +++ b/front_end/src/stories/feed_card/binary_question/mock_data.ts @@ -35,8 +35,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/mcup-banner.jpg", prize_pool: "5000.00", start_date: "2025-05-02T17:00:00Z", - close_date: "2025-09-04T00:00:00Z", - forecasting_end_date: "2025-09-01T00:00:00Z", + winners_announced_date: "2025-09-04T00:00:00Z", + close_date: "2025-09-01T00:00:00Z", meta_description: "", is_ongoing: true, user_permission: null, @@ -56,8 +56,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/mcup-banner.jpg", prize_pool: "5000.00", start_date: "2025-05-02T17:00:00Z", - close_date: "2025-09-04T00:00:00Z", - forecasting_end_date: "2025-09-01T00:00:00Z", + winners_announced_date: "2025-09-04T00:00:00Z", + close_date: "2025-09-01T00:00:00Z", meta_description: "", is_ongoing: true, user_permission: null, diff --git a/front_end/src/stories/feed_card/continuous_question/mock_data.ts b/front_end/src/stories/feed_card/continuous_question/mock_data.ts index f6a950ea33..4515a24806 100644 --- a/front_end/src/stories/feed_card/continuous_question/mock_data.ts +++ b/front_end/src/stories/feed_card/continuous_question/mock_data.ts @@ -61,8 +61,8 @@ export const getMockData = (isResolved: boolean) => { header_image: null, prize_pool: null, start_date: "2025-06-18T13:00:10Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: "admin", @@ -81,8 +81,8 @@ export const getMockData = (isResolved: boolean) => { header_image: null, prize_pool: null, start_date: "2025-06-19T16:05:02Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: null, @@ -102,8 +102,8 @@ export const getMockData = (isResolved: boolean) => { header_image: null, prize_pool: null, start_date: "2025-06-19T16:05:02Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: null, diff --git a/front_end/src/stories/feed_card/date_group/mock_data.tsx b/front_end/src/stories/feed_card/date_group/mock_data.tsx index 6099a3b02b..deffb8b18d 100644 --- a/front_end/src/stories/feed_card/date_group/mock_data.tsx +++ b/front_end/src/stories/feed_card/date_group/mock_data.tsx @@ -51,8 +51,8 @@ export const getMockData = (isResolved: boolean, isFanChart?: boolean) => { header_image: null, prize_pool: null, start_date: null, + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: null, user_permission: null, @@ -72,8 +72,8 @@ export const getMockData = (isResolved: boolean, isFanChart?: boolean) => { header_image: null, prize_pool: null, start_date: null, + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: null, user_permission: null, diff --git a/front_end/src/stories/feed_card/date_question/mock_data.ts b/front_end/src/stories/feed_card/date_question/mock_data.ts index 011be17a82..e76be21f36 100644 --- a/front_end/src/stories/feed_card/date_question/mock_data.ts +++ b/front_end/src/stories/feed_card/date_question/mock_data.ts @@ -53,8 +53,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/neural_net.png", prize_pool: "6500.00", start_date: "2022-02-08T13:02:52Z", - close_date: "2022-04-16T15:54:55Z", - forecasting_end_date: null, + winners_announced_date: "2022-04-16T15:54:55Z", + close_date: null, meta_description: "Welcome to the AI Progress Essay Contest! Metaculus aims to support accurate forecasting of and preparation for the impacts of transformative AI.\r\n\r\nThank you for your participation!", is_ongoing: false, @@ -74,8 +74,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/agi.png", prize_pool: null, start_date: "2023-08-09T13:41:42.701000Z", - close_date: "2023-08-09T13:41:42.701000Z", - forecasting_end_date: null, + winners_announced_date: "2023-08-09T13:41:42.701000Z", + close_date: null, meta_description: "", is_ongoing: false, user_permission: null, @@ -109,8 +109,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/agi.png", prize_pool: null, start_date: "2023-08-09T13:41:42.701000Z", - close_date: "2023-08-09T13:41:42.701000Z", - forecasting_end_date: null, + winners_announced_date: "2023-08-09T13:41:42.701000Z", + close_date: null, meta_description: "", is_ongoing: false, user_permission: null, @@ -148,8 +148,8 @@ export const getMockData = (isResolved: boolean) => { header_image: null, prize_pool: null, start_date: null, + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: null, user_permission: "admin", diff --git a/front_end/src/stories/feed_card/mc_question/mock_data.ts b/front_end/src/stories/feed_card/mc_question/mock_data.ts index ab0448fb12..1490d16e8c 100644 --- a/front_end/src/stories/feed_card/mc_question/mock_data.ts +++ b/front_end/src/stories/feed_card/mc_question/mock_data.ts @@ -54,8 +54,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/2027-banner.jpg", prize_pool: null, start_date: "2025-06-10T00:00:00Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: null, @@ -75,8 +75,8 @@ export const getMockData = (isResolved: boolean) => { header_image: "https://cdn.metaculus.com/2027-banner.jpg", prize_pool: null, start_date: "2025-06-10T00:00:00Z", + winners_announced_date: null, close_date: null, - forecasting_end_date: null, meta_description: "", is_ongoing: true, user_permission: null, diff --git a/front_end/src/stories/feed_card/numeric_group/mock_data.ts b/front_end/src/stories/feed_card/numeric_group/mock_data.ts index ab7dce5802..e8489e7b04 100644 --- a/front_end/src/stories/feed_card/numeric_group/mock_data.ts +++ b/front_end/src/stories/feed_card/numeric_group/mock_data.ts @@ -35,8 +35,8 @@ export const getMockData = (isResolved: boolean, isFanChart?: boolean) => { header_image: "https://cdn.metaculus.com/potus-banner0.5x.webp", prize_pool: "15000.00", start_date: "2025-03-28T12:00:00Z", - close_date: "2029-01-20T00:00:00Z", - forecasting_end_date: "2029-01-03T00:00:00Z", + winners_announced_date: "2029-01-20T00:00:00Z", + close_date: "2029-01-03T00:00:00Z", meta_description: "", is_ongoing: true, user_permission: null, @@ -56,8 +56,8 @@ export const getMockData = (isResolved: boolean, isFanChart?: boolean) => { header_image: "https://cdn.metaculus.com/potus-banner0.5x.webp", prize_pool: "15000.00", start_date: "2025-03-28T12:00:00Z", - close_date: "2029-01-20T00:00:00Z", - forecasting_end_date: "2029-01-03T00:00:00Z", + winners_announced_date: "2029-01-20T00:00:00Z", + close_date: "2029-01-03T00:00:00Z", meta_description: "", is_ongoing: true, user_permission: null, diff --git a/front_end/src/types/projects.ts b/front_end/src/types/projects.ts index 9a8e303111..06d040d8b4 100644 --- a/front_end/src/types/projects.ts +++ b/front_end/src/types/projects.ts @@ -45,7 +45,7 @@ export enum TaxonomyProjectType { export enum TournamentsSortBy { Featured = "featured", PrizePoolDesc = "-prize_pool", - CloseDateAsc = "close_date", + WinnersAnnouncedDateAsc = "winners_announced_date", StartDateDesc = "-start_date", } @@ -62,8 +62,8 @@ export type TournamentPreview = Project & { forecasters_count: number; prize_pool: string | null; start_date: string; + winners_announced_date?: string; close_date?: string; - forecasting_end_date?: string; is_ongoing: boolean; created_at: string; questions_count: number; diff --git a/posts/services/common.py b/posts/services/common.py index 169c198772..61b110561b 100644 --- a/posts/services/common.py +++ b/posts/services/common.py @@ -19,7 +19,7 @@ from projects.services.common import ( get_projects_staff_users, get_site_main_project, - move_project_forecasting_end_date, + move_project_close_date, ) from questions.models import Question from questions.services.common import ( @@ -432,7 +432,7 @@ def approve_post( Project.ProjectTypes.TOURNAMENT, Project.ProjectTypes.QUESTION_SERIES, ]: - move_project_forecasting_end_date(project, post) + move_project_close_date(project, post) post.update_pseudo_materialized_fields() diff --git a/projects/admin.py b/projects/admin.py index f356b9928c..28b0bfc1e0 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -187,7 +187,7 @@ def closes_before(self, obj): if project.primary_leaderboard else None ) - finalize_time = finalize_time or project.close_date + finalize_time = finalize_time or project.winners_announced_date if not finalize_time: return True @@ -211,7 +211,7 @@ def resolved_before(self, obj): if project.primary_leaderboard else None ) - finalize_time = finalize_time or project.close_date + finalize_time = finalize_time or project.winners_announced_date if not finalize_time: return True @@ -368,19 +368,19 @@ def __init__(self, *args, **kwargs): def clean(self): cleaned_data = super().clean() start_date = cleaned_data.get("start_date") - forecasting_end_date = cleaned_data.get("forecasting_end_date") close_date = cleaned_data.get("close_date") + winners_announced_date = cleaned_data.get("winners_announced_date") - if forecasting_end_date and close_date and forecasting_end_date > close_date: + if close_date and winners_announced_date and close_date > winners_announced_date: self.add_error( - "forecasting_end_date", - "Forecasting end date must be before the close date.", + "close_date", + "Close date must be before the winners announced date.", ) - if start_date and forecasting_end_date and start_date > forecasting_end_date: + if start_date and close_date and start_date > close_date: self.add_error( "start_date", - "Start date must be before the forecasting end date.", + "Start date must be before the close date.", ) return cleaned_data @@ -844,7 +844,7 @@ def questions_summary(self, obj): all_ids = list(all_questions.values_list("id", flat=True)) all_count = len(all_ids) - finalize_time = leaderboard.finalize_time or obj.close_date + finalize_time = leaderboard.finalize_time or obj.winners_announced_date if finalize_time: in_leaderboard_qs = all_questions.filter( Q(resolution_set_time__isnull=True) @@ -920,7 +920,7 @@ def primary_leaderboard_finalize_time(self, obj): return None leaderboard = obj.primary_leaderboard if leaderboard: - return leaderboard.finalize_time or (obj.close_date if obj else None) + return leaderboard.finalize_time or (obj.winners_announced_date if obj else None) primary_leaderboard_finalize_time.short_description = ( "Time when Primary Leaderboard is Finalized" diff --git a/projects/migrations/0024_rename_project_date_fields.py b/projects/migrations/0024_rename_project_date_fields.py new file mode 100644 index 0000000000..761368de89 --- /dev/null +++ b/projects/migrations/0024_rename_project_date_fields.py @@ -0,0 +1,53 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0023_alter_project_bot_leaderboard_status"), + ] + + operations = [ + # Swap close_date and forecasting_end_date via a temporary name. + # close_date (old: tournament wrap-up) -> winners_announced_date + # forecasting_end_date (old: last scored question close) -> close_date + migrations.RenameField( + model_name="project", + old_name="close_date", + new_name="_winners_announced_date_tmp", + ), + migrations.RenameField( + model_name="project", + old_name="forecasting_end_date", + new_name="close_date", + ), + migrations.RenameField( + model_name="project", + old_name="_winners_announced_date_tmp", + new_name="winners_announced_date", + ), + migrations.AlterField( + model_name="project", + name="winners_announced_date", + field=models.DateTimeField( + blank=True, + help_text=( + "The date the tournament wraps up and prizes will be paid. " + "All questions that should be included in the leaderboard must close and resolve before this date." + ), + null=True, + ), + ), + migrations.AlterField( + model_name="project", + name="close_date", + field=models.DateTimeField( + blank=True, + help_text=( + "The date the last question that counts for the tournament is scheduled to close. " + "If such questions have scheduled close dates after the project Winners announced date, " + "the front end will show the latest of these close dates." + ), + null=True, + ), + ), + ] diff --git a/projects/models.py b/projects/models.py index 5df748a39d..7c4897e715 100644 --- a/projects/models.py +++ b/projects/models.py @@ -291,22 +291,21 @@ class BotLeaderboardStatus(models.TextChoices): default=None, decimal_places=2, max_digits=15, null=True, blank=True ) start_date = models.DateTimeField(null=True, blank=True) - close_date = models.DateTimeField( + winners_announced_date = models.DateTimeField( null=True, blank=True, help_text=( "The date the tournament wraps up and prizes will be paid. " - "All questions that should be included in the leaderboard must close and resolve before this date. " - "This is displayed on the front end as the “Winners announced date”" + "All questions that should be included in the leaderboard must close and resolve before this date." ), ) - forecasting_end_date = models.DateTimeField( + close_date = models.DateTimeField( null=True, blank=True, help_text=( - "The date the last scored question that counts for the tournament closes." - "The date shown is the latest of the Forecasting end date " - "and the latest question close date closing and resolving before the Close date." + "The date the last question that counts for the tournament is scheduled to close. " + "If such questions have scheduled close dates after the project Winners announced date, " + "the front end will show the latest of these close dates." ), ) sign_up_fields = models.JSONField( @@ -443,7 +442,11 @@ def is_ongoing(self): self.ProjectTypes.QUESTION_SERIES, self.ProjectTypes.INDEX, ): - return self.close_date > django_timezone.now() if self.close_date else True + return ( + self.winners_announced_date > django_timezone.now() + if self.winners_announced_date + else True + ) def get_users_for_permission(self, min_permission: ObjectPermission): """ diff --git a/projects/serializers/common.py b/projects/serializers/common.py index 6b9f6e58a8..997ff2340a 100644 --- a/projects/serializers/common.py +++ b/projects/serializers/common.py @@ -35,8 +35,8 @@ class Meta: "order", "prize_pool", "start_date", + "winners_announced_date", "close_date", - "forecasting_end_date", "html_metadata_json", "default_permission", "visibility", @@ -90,8 +90,8 @@ class Meta: "header_image", "prize_pool", "start_date", + "winners_announced_date", "close_date", - "forecasting_end_date", "html_metadata_json", "is_ongoing", "user_permission", diff --git a/projects/services/common.py b/projects/services/common.py index 432a5fad57..3b8e64fb30 100644 --- a/projects/services/common.py +++ b/projects/services/common.py @@ -137,25 +137,25 @@ def get_projects_for_posts( return post_projects_map -def move_project_forecasting_end_date(project: Project, post: Post): - if not project.close_date: +def move_project_close_date(project: Project, post: Post): + if not project.winners_announced_date: return - forecasting_end_date = project.forecasting_end_date + close_date = project.close_date for question in post.get_questions(): if ( - question.scheduled_close_time <= project.close_date - and question.scheduled_resolve_time <= project.close_date + question.scheduled_close_time <= project.winners_announced_date + and question.scheduled_resolve_time <= project.winners_announced_date and ( - not forecasting_end_date - or question.scheduled_close_time > forecasting_end_date + not close_date + or question.scheduled_close_time > close_date ) ): - forecasting_end_date = question.scheduled_close_time + close_date = question.scheduled_close_time - project.forecasting_end_date = forecasting_end_date - project.save(update_fields=["forecasting_end_date"]) + project.close_date = close_date + project.save(update_fields=["close_date"]) def get_questions_by_project( @@ -213,15 +213,15 @@ def _calculate_timeline_data(project: Project, questions: Iterable[Question]) -> actual_resolve_times = [] scheduled_resolve_times = [] - project_close_date = project.close_date or make_aware(datetime.max) - project_forecasting_end_date = project.forecasting_end_date or project_close_date + project_winners_announced_date = project.winners_announced_date or make_aware(datetime.max) + project_close_date = project.close_date or project_winners_announced_date for question in questions: if all_questions_resolved: all_questions_resolved = ( question.actual_resolve_time # Or treat as resolved as scheduled resolution is in the future - or question.scheduled_resolve_time > project_close_date + or question.scheduled_resolve_time > project_winners_announced_date ) # Determine questions closure @@ -229,7 +229,7 @@ def _calculate_timeline_data(project: Project, questions: Iterable[Question]) -> close_time = question.actual_close_time or question.scheduled_close_time all_questions_closed = ( close_time <= timezone.now() - or close_time > project_forecasting_end_date + or close_time > project_close_date ) if question.cp_reveal_time: @@ -245,7 +245,7 @@ def _calculate_timeline_data(project: Project, questions: Iterable[Question]) -> scheduled_resolve_times.append(scheduled_resolve_time) def get_max(data: list): - return max([x for x in data if x <= project_close_date], default=None) + return max([x for x in data if x <= project_winners_announced_date], default=None) return { "last_cp_reveal_time": get_max(cp_reveal_times), @@ -349,13 +349,13 @@ def get_feed_project_tiles() -> list[dict]: for q in questions if q.resolution_set_time and q.resolution_set_time >= three_days_ago ) - project_close_date = project.close_date or make_aware(datetime.max) + project_winners_announced_date = project.winners_announced_date or make_aware(datetime.max) all_resolved = len(questions) > 0 and all( q.actual_resolve_time # Or treat as resolved if scheduled resolution is in the future or ( q.scheduled_resolve_time - and q.scheduled_resolve_time > project_close_date + and q.scheduled_resolve_time > project_winners_announced_date ) for q in questions ) @@ -365,12 +365,12 @@ def get_feed_project_tiles() -> list[dict]: for q in questions if q.actual_resolve_time and q.resolution_set_time - and q.actual_resolve_time <= project_close_date + and q.actual_resolve_time <= project_winners_announced_date ), default=None, ) project_resolution_date = ( - max(last_resolve_time, project.close_date or last_resolve_time) + max(last_resolve_time, project.winners_announced_date or last_resolve_time) if last_resolve_time else None ) @@ -411,9 +411,9 @@ def get_feed_project_tiles() -> list[dict]: p for p in projects if p.start_date - and p.close_date - and (total := (p.close_date - p.start_date).total_seconds()) > 0 - and (p.close_date - now).total_seconds() / total > 0.5 + and p.winners_announced_date + and (total := (p.winners_announced_date - p.start_date).total_seconds()) > 0 + and (p.winners_announced_date - now).total_seconds() / total > 0.5 ] if candidates: pick = random.choice(candidates) diff --git a/scoring/jobs.py b/scoring/jobs.py index 65d671c01b..6e4f2296be 100644 --- a/scoring/jobs.py +++ b/scoring/jobs.py @@ -46,7 +46,7 @@ def finalize_leaderboards(): active_leaderboards = Leaderboard.objects.filter(finalized=False) for leaderboard in active_leaderboards: finalize_time = leaderboard.finalize_time or ( - leaderboard.project.close_date if leaderboard.project else None + leaderboard.project.winners_announced_date if leaderboard.project else None ) if finalize_time and finalize_time <= timezone.now(): logger.info(f"Finalizing leaderboard: {leaderboard}") diff --git a/scoring/models.py b/scoring/models.py index 926ebdb405..30d34ca654 100644 --- a/scoring/models.py +++ b/scoring/models.py @@ -177,7 +177,7 @@ class Leaderboard(TimeStampedModel): finalize_time = models.DateTimeField( null=True, blank=True, - help_text="""Optional. If not set, the Project's close_date will be used instead. + help_text="""Optional. If not set, the Project's winners_announced_date will be used instead.
- For all Leaderboards: used to filter out questions that have a resolution_set_time after this (as they were resolved after this Leaderboard was finalized).
- Filtering MedalExclusionRecords: If set and end_time is not set, MedalExclusionRecords that have a start_time less than this (and no end_time or an end_time later that this Leaderboard's start_time) will be triggered. """, diff --git a/scoring/utils.py b/scoring/utils.py index 4e287fbd7c..7d5142ee9c 100644 --- a/scoring/utils.py +++ b/scoring/utils.py @@ -152,7 +152,7 @@ def retrieve_question_scores( } finalize_time = leaderboard.finalize_time or ( - leaderboard.project.close_date if leaderboard.project else None + leaderboard.project.winners_announced_date if leaderboard.project else None ) if finalize_time: qs_filters["question__scheduled_close_time__lte"] = finalize_time @@ -446,10 +446,10 @@ def assign_exclusions_( leaderboard.project.start_date if leaderboard.project else None ) end_time = leaderboard.end_time or ( - leaderboard.project.close_date if leaderboard.project else None + leaderboard.project.winners_announced_date if leaderboard.project else None ) finalize_time = leaderboard.finalize_time or ( - leaderboard.project.close_date if leaderboard.project else None + leaderboard.project.winners_announced_date if leaderboard.project else None ) if start_time: exclusion_records = exclusion_records.filter( @@ -592,7 +592,7 @@ def calculate_medals_points_at_time(at_time): # Look only at leaderboard entries which are closed before the timestamp relevant_entries_qs = LeaderboardEntry.objects.filter( Q(leaderboard__end_time__lte=at_time) - | Q(leaderboard__project__close_date__lte=at_time), + | Q(leaderboard__project__winners_announced_date__lte=at_time), leaderboard__project__default_permission=ObjectPermission.FORECASTER, ) @@ -601,7 +601,7 @@ def calculate_medals_points_at_time(at_time): leaderboard_age_expr = Case( When( leaderboard__project__type="tournament", - then=(at_time.year - ExtractYear(F("leaderboard__project__close_date"))), + then=(at_time.year - ExtractYear(F("leaderboard__project__winners_announced_date"))), ), When( leaderboard__end_time__isnull=False, @@ -779,7 +779,7 @@ def process_entries_for_leaderboard_( assign_prizes_(entries, prize_pool) # check if we're ready to finalize and assign medals/prizes if applicable finalize_time = leaderboard.finalize_time or ( - project.close_date if project else None + project.winners_announced_date if project else None ) if force_finalize or (finalize_time and (timezone.now() >= finalize_time)): if (