diff --git a/front_end/messages/en.json b/front_end/messages/en.json index ba6d2cfd89..8f760c0469 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -14,6 +14,7 @@ "predictionUpcomingMessage": "This question is in the Upcoming stage, and will be open for predictions soon", "predictionUnapprovedMessage": "This question must be approved by moderators before you can begin predicting", "predictionClosedMessage": "This question is closed for predictions, and is waiting to be resolved", + "predictionConditionalPartiallyClosedMessage": "The question \"{closedQuestion}\" has closed, so these conditionals are closed for forecasting. They will resolve when the question \"{activeQuestion}\" resolves.", "resolve": "Resolve", "unresolve": "Unresolve", "preview": "Preview", diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_binary.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_binary.tsx index 4388d756da..e678b36ced 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_binary.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_binary.tsx @@ -2,7 +2,7 @@ import classNames from "classnames"; import { round } from "lodash"; import { useTranslations } from "next-intl"; -import React, { FC, useCallback, useMemo, useState } from "react"; +import React, { FC, ReactNode, useCallback, useMemo, useState } from "react"; import { createForecasts } from "@/app/(main)/questions/actions"; import Button from "@/components/ui/button"; @@ -10,10 +10,7 @@ import { FormErrorMessage } from "@/components/ui/form_field"; import { useAuth } from "@/contexts/auth_context"; import { ErrorResponse } from "@/types/fetch"; import { Post, PostConditional } from "@/types/post"; -import { - PredictionInputMessage, - QuestionWithNumericForecasts, -} from "@/types/question"; +import { QuestionWithNumericForecasts } from "@/types/question"; import { extractPrevBinaryForecastValue } from "@/utils/forecasts"; import { sendGAConditionalPredictEvent } from "./ga_events"; @@ -32,7 +29,7 @@ type Props = { prevYesForecast?: any; prevNoForecast?: any; canPredict: boolean; - predictionMessage: PredictionInputMessage; + predictionMessage: ReactNode; projects: Post["projects"]; }; @@ -276,7 +273,7 @@ const ForecastMakerConditionalBinary: FC = ({
{predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)} {canPredict && ( diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_continuous.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_continuous.tsx index 43eb6ecbd6..5611dc0d1f 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_continuous.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/forecast_maker_conditional_continuous.tsx @@ -1,7 +1,7 @@ "use client"; import classNames from "classnames"; import { useTranslations } from "next-intl"; -import React, { FC, useCallback, useMemo, useState } from "react"; +import React, { FC, ReactNode, useCallback, useMemo, useState } from "react"; import { createForecasts } from "@/app/(main)/questions/actions"; import { MultiSliderValue } from "@/components/sliders/multi_slider"; @@ -10,11 +10,7 @@ import { FormErrorMessage } from "@/components/ui/form_field"; import { useAuth } from "@/contexts/auth_context"; import { ErrorResponse } from "@/types/fetch"; import { Post, PostConditional } from "@/types/post"; -import { - PredictionInputMessage, - Quartiles, - QuestionWithNumericForecasts, -} from "@/types/question"; +import { Quartiles, QuestionWithNumericForecasts } from "@/types/question"; import { getDisplayValue } from "@/utils/charts"; import { extractPrevNumericForecastValue, @@ -39,7 +35,7 @@ type Props = { prevYesForecast?: any; prevNoForecast?: any; canPredict: boolean; - predictionMessage: PredictionInputMessage; + predictionMessage: ReactNode; projects: Post["projects"]; }; @@ -388,7 +384,7 @@ const ForecastMakerConditionalContinuous: FC = ({ ))} {predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)} {canPredict && ( diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/index.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/index.tsx index 869674a6c9..d45f9bfdae 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_conditional/index.tsx @@ -1,9 +1,8 @@ import { useTranslations } from "next-intl"; -import { FC } from "react"; +import { FC, ReactNode } from "react"; import { PostConditional, PostWithForecasts } from "@/types/post"; import { - PredictionInputMessage, QuestionType, QuestionWithForecasts, QuestionWithNumericForecasts, @@ -17,7 +16,7 @@ type Props = { post: PostWithForecasts; conditional: PostConditional; canPredict: boolean; - predictionMessage: PredictionInputMessage; + predictionMessage: ReactNode; }; const ForecastMakerConditional: FC = ({ @@ -41,6 +40,7 @@ const ForecastMakerConditional: FC = ({ : false; const conditionClosedOrResolved = parentSuccessfullyResolved || parentIsClosed; + return ( = ({ @@ -268,7 +268,7 @@ const ForecastMakerGroupBinary: FC = ({ {predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)} {!!highlightedQuestion?.resolution && ( diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx index 4ed6b75fe4..4aa00c4d67 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx @@ -6,7 +6,14 @@ import { differenceInMilliseconds } from "date-fns"; import { isNil } from "lodash"; import { useSearchParams } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; -import React, { FC, useCallback, useEffect, useMemo, useState } from "react"; +import React, { + FC, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { createForecasts } from "@/app/(main)/questions/actions"; import { MultiSliderValue } from "@/components/sliders/multi_slider"; @@ -51,7 +58,7 @@ type Props = { groupVariable: string; canPredict: boolean; canResolve: boolean; - predictionMessage: PredictionInputMessage; + predictionMessage: ReactNode; }; const ForecastMakerGroupContinuous: FC = ({ @@ -338,7 +345,7 @@ const ForecastMakerGroupContinuous: FC = ({ })} {predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)} {!!activeGroupOption && diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/index.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/index.tsx index 82cf1f541c..3f6e0118e6 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/index.tsx @@ -1,9 +1,8 @@ import { useTranslations } from "next-intl"; -import { FC } from "react"; +import { FC, ReactNode } from "react"; import { PostWithForecasts } from "@/types/post"; import { - PredictionInputMessage, QuestionType, QuestionWithForecasts, QuestionWithNumericForecasts, @@ -22,7 +21,7 @@ type Props = { groupVariable: string; canPredict: boolean; canResolve: boolean; - predictionMessage: PredictionInputMessage; + predictionMessage: ReactNode; }; const ForecastMakerGroup: FC = ({ diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_binary.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_binary.tsx index 1aaa73bfbe..3b0b046539 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_binary.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_binary.tsx @@ -1,7 +1,7 @@ "use client"; import { round } from "lodash"; import { useTranslations } from "next-intl"; -import React, { FC, useEffect, useState } from "react"; +import React, { FC, ReactNode, useEffect, useState } from "react"; import { createForecasts } from "@/app/(main)/questions/actions"; import { FormErrorMessage } from "@/components/ui/form_field"; @@ -10,10 +10,7 @@ import { useAuth } from "@/contexts/auth_context"; import { useServerAction } from "@/hooks/use_server_action"; import { ErrorResponse } from "@/types/fetch"; import { PostWithForecasts, ProjectPermissions } from "@/types/post"; -import { - PredictionInputMessage, - QuestionWithNumericForecasts, -} from "@/types/question"; +import { QuestionWithNumericForecasts } from "@/types/question"; import { extractPrevBinaryForecastValue } from "@/utils/forecasts"; import { sendGAPredictEvent } from "./ga_events"; @@ -30,7 +27,7 @@ type Props = { permission?: ProjectPermissions; canPredict: boolean; canResolve: boolean; - predictionMessage?: PredictionInputMessage; + predictionMessage?: ReactNode; }; const ForecastMakerBinary: FC = ({ @@ -99,7 +96,7 @@ const ForecastMakerBinary: FC = ({ /> {predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)}
diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx index 1411baa6d4..ceeadea7a3 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx @@ -1,6 +1,6 @@ "use client"; import { useTranslations } from "next-intl"; -import React, { FC, useMemo, useState } from "react"; +import React, { FC, ReactNode, useMemo, useState } from "react"; import { createForecasts } from "@/app/(main)/questions/actions"; import { MultiSliderValue } from "@/components/sliders/multi_slider"; @@ -11,10 +11,7 @@ import { useAuth } from "@/contexts/auth_context"; import { useServerAction } from "@/hooks/use_server_action"; import { ErrorResponse } from "@/types/fetch"; import { PostWithForecasts, ProjectPermissions } from "@/types/post"; -import { - PredictionInputMessage, - QuestionWithNumericForecasts, -} from "@/types/question"; +import { QuestionWithNumericForecasts } from "@/types/question"; import { extractPrevNumericForecastValue, getNumericForecastDataset, @@ -36,7 +33,7 @@ type Props = { permission?: ProjectPermissions; canPredict: boolean; canResolve: boolean; - predictionMessage?: PredictionInputMessage; + predictionMessage?: ReactNode; }; const ForecastMakerContinuous: FC = ({ @@ -177,7 +174,7 @@ const ForecastMakerContinuous: FC = ({ )} {predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)} = ({ @@ -262,7 +261,7 @@ const ForecastMakerMultipleChoice: FC = ({ {predictionMessage && (
- {t(predictionMessage)} + {predictionMessage}
)}
diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/index.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/index.tsx index 5cb39f5af3..e68d7b6793 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/index.tsx @@ -1,13 +1,9 @@ import { parseISO } from "date-fns"; import { useLocale, useTranslations } from "next-intl"; -import { FC } from "react"; +import { FC, ReactNode } from "react"; import { PostWithForecasts, ProjectPermissions } from "@/types/post"; -import { - PredictionInputMessage, - QuestionType, - QuestionWithForecasts, -} from "@/types/question"; +import { QuestionType, QuestionWithForecasts } from "@/types/question"; import { formatResolution } from "@/utils/questions"; import ForecastMakerBinary from "./forecast_maker_binary"; @@ -22,7 +18,7 @@ type Props = { permission?: ProjectPermissions; canPredict: boolean; canResolve: boolean; - predictionMessage: PredictionInputMessage; + predictionMessage: ReactNode; }; const QuestionForecastMaker: FC = ({ diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/index.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/index.tsx index 35085c4f7f..4816d8cba4 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/index.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/index.tsx @@ -2,15 +2,13 @@ import { parseISO } from "date-fns"; import { isNil } from "lodash"; import { FC } from "react"; +import PredictionStatusMessage from "@/app/(main)/questions/[id]/components/forecast_maker/prediction_status_message"; import { PostStatus, PostWithForecasts, ProjectPermissions, } from "@/types/post"; -import { - canPredictQuestion, - getPredictionInputMessage, -} from "@/utils/questions"; +import { canPredictQuestion } from "@/utils/questions"; import ForecastMakerConditional from "./forecast_maker_conditional"; import ForecastMakerGroup from "./forecast_maker_group"; @@ -37,7 +35,8 @@ const ForecastMaker: FC = ({ post }) => { parseISO(post.published_at) <= new Date() && [PostStatus.APPROVED, PostStatus.OPEN, PostStatus.CLOSED].includes(status); - const predictionMessage = getPredictionInputMessage(post); + const predictionMessage = ; + if (groupOfQuestions) { return ( = ({ post }) => { + const t = useTranslations(); + + switch (post.status) { + case PostStatus.UPCOMING: { + return <>{t("predictionUpcomingMessage")}; + } + case PostStatus.APPROVED: { + if (Date.parse(post.open_time) > Date.now()) { + return <>{t("predictionUpcomingMessage")}; + } + return null; + } + case PostStatus.REJECTED: + case PostStatus.PENDING: + case PostStatus.DRAFT: { + return <>{t("predictionUnapprovedMessage")}; + } + case PostStatus.CLOSED: { + if (!post.resolved) { + if (post.conditional) { + const questions = [ + post.conditional.condition, + post.conditional.condition_child, + ]; + const targetStatuses = [ + QuestionStatus.CLOSED, + QuestionStatus.RESOLVED, + ]; + + const closedQuestion = questions.find((obj) => + targetStatuses.includes(obj.status!) + ); + const activeQuestion = questions.find( + (obj) => !targetStatuses.includes(obj.status!) + ); + + if (closedQuestion && activeQuestion) { + return ( + <> + {t("predictionConditionalPartiallyClosedMessage", { + closedQuestion: closedQuestion.title, + activeQuestion: activeQuestion.title, + })} + + ); + } + } + + return <>{t("predictionClosedMessage")}; + } + } + } + + return null; +}; + +export default PredictionStatusMessage; diff --git a/posts/models.py b/posts/models.py index 62eaad1b3f..bd83bc3201 100644 --- a/posts/models.py +++ b/posts/models.py @@ -502,7 +502,16 @@ def set_actual_close_time(self): else: self.actual_close_time = max(close_times) elif self.conditional: - self.actual_close_time = self.conditional.condition_child.actual_close_time + self.actual_close_time = min( + filter( + bool, + [ + self.conditional.condition.actual_close_time, + self.conditional.condition_child.actual_close_time, + ], + ), + default=None, + ) else: self.actual_close_time = None @@ -533,15 +542,25 @@ def set_open_time(self): open_time = self.question.open_time elif self.conditional_id: open_time = max( - self.conditional.condition.open_time, - self.conditional.condition_child.open_time, + filter( + bool, + [ + self.conditional.condition.open_time, + self.conditional.condition_child.open_time, + ], + ), + default=None, ) - elif self.group_of_questions_id: - questions = self.group_of_questions.questions.all() - open_times = [x.open_time for x in questions if x.open_time] - if open_times: - open_time = min(open_times) + elif self.group_of_questions_id: + open_time = min( + [ + x.open_time + for x in self.group_of_questions.questions.all() + if x.open_time + ], + default=None, + ) self.open_time = open_time diff --git a/posts/serializers.py b/posts/serializers.py index aa8ef2943c..0b57e594be 100644 --- a/posts/serializers.py +++ b/posts/serializers.py @@ -102,7 +102,9 @@ def get_status(self, obj: Post): if not obj.open_time or obj.open_time > now: return Post.CurationStatus.APPROVED - if now < obj.scheduled_close_time: + if now < obj.scheduled_close_time and ( + not obj.actual_close_time or now < obj.actual_close_time + ): return Post.PostStatusChange.OPEN return Post.PostStatusChange.CLOSED diff --git a/pytest.ini b/pytest.ini index 09a6da0d1d..24d83491e1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] DJANGO_SETTINGS_MODULE = metaculus_web.test_settings -addopts = --showlocals --create-db +addopts = --showlocals --reuse-db env = D:METACULUS_ENV=testing diff --git a/tests/unit/test_comments/test_serializers.py b/tests/unit/test_comments/test_serializers.py index dfea7e06e9..820a6b3dc1 100644 --- a/tests/unit/test_comments/test_serializers.py +++ b/tests/unit/test_comments/test_serializers.py @@ -4,7 +4,7 @@ from tests.unit.fixtures import * # noqa from tests.unit.test_comments.factories import factory_comment, factory_key_factor from tests.unit.test_posts.factories import factory_post -from tests.unit.test_questions.fixtures import * # noqa +from tests.unit.test_questions.conftest import * # noqa from tests.unit.test_users.factories import factory_user diff --git a/tests/unit/test_comments/test_services.py b/tests/unit/test_comments/test_services.py index 3c31c39f98..41746978f0 100644 --- a/tests/unit/test_comments/test_services.py +++ b/tests/unit/test_comments/test_services.py @@ -10,7 +10,7 @@ from tests.unit.test_posts.factories import factory_post from tests.unit.test_projects.factories import factory_project from tests.unit.test_questions.factories import factory_forecast -from tests.unit.test_questions.fixtures import * # noqa +from tests.unit.test_questions.conftest import * # noqa from tests.unit.test_users.factories import factory_user diff --git a/tests/unit/test_posts/test_models.py b/tests/unit/test_posts/test_models.py index 954083155b..3fdd8e1302 100644 --- a/tests/unit/test_posts/test_models.py +++ b/tests/unit/test_posts/test_models.py @@ -11,7 +11,7 @@ from tests.unit.test_posts.factories import factory_post, factory_post_snapshot from tests.unit.test_projects.factories import factory_project from tests.unit.test_questions.factories import factory_forecast -from tests.unit.test_questions.fixtures import * # noqa +from tests.unit.test_questions.conftest import * # noqa from tests.unit.test_users.factories import factory_user diff --git a/tests/unit/test_posts/test_views.py b/tests/unit/test_posts/test_views.py index 5fb1a19afe..44ae40f7d8 100644 --- a/tests/unit/test_posts/test_views.py +++ b/tests/unit/test_posts/test_views.py @@ -16,7 +16,7 @@ from tests.unit.test_posts.factories import factory_post from tests.unit.test_projects.factories import factory_project from tests.unit.test_questions.factories import create_question -from tests.unit.test_questions.fixtures import * # noqa +from tests.unit.test_questions.conftest import * # noqa class TestPostCreate: diff --git a/tests/unit/test_questions/fixtures.py b/tests/unit/test_questions/conftest.py similarity index 100% rename from tests/unit/test_questions/fixtures.py rename to tests/unit/test_questions/conftest.py diff --git a/tests/unit/test_questions/test_services.py b/tests/unit/test_questions/test_services.py new file mode 100644 index 0000000000..69be41a1e6 --- /dev/null +++ b/tests/unit/test_questions/test_services.py @@ -0,0 +1,200 @@ +from datetime import datetime + +import freezegun +import pytest # noqa +from django.utils.timezone import make_aware + +from posts.services.common import create_post, approve_post +from questions.models import Question +from questions.services import resolve_question, unresolve_question +from tests.unit.fixtures import * # noqa +from tests.unit.test_posts.factories import factory_post +from tests.unit.test_questions.factories import create_question + + +@freezegun.freeze_time("2024-11-1") +class TestResolveConditionalQuestion: + @pytest.fixture() + def post_parent(self, user1): + return factory_post( + author=user1, + question=create_question( + question_type=Question.QuestionType.BINARY, + open_time=make_aware(datetime(2023, 1, 1)), + scheduled_close_time=make_aware(datetime(2025, 1, 1)), + ), + ) + + @pytest.fixture() + def post_child(self, user1): + return factory_post( + author=user1, + question=create_question( + question_type=Question.QuestionType.BINARY, + open_time=make_aware(datetime(2023, 1, 1)), + scheduled_close_time=make_aware(datetime(2025, 1, 1)), + ), + ) + + @pytest.fixture() + def post_conditional(self, user1, post_parent, post_child): + post = create_post( + conditional={ + "condition_id": post_parent.question_id, + "condition_child_id": post_child.question_id, + }, + author=user1, + ) + approve_post( + post, make_aware(datetime(2024, 10, 1)), make_aware(datetime(2024, 10, 1)) + ) + post.update_pseudo_materialized_fields() + + return post + + def test_conditional_child_and_parent_resolved( + self, post_conditional, post_child, post_parent + ): + """ + Both Condition and Child are resolved + """ + + assert not post_conditional.resolved + + # Close child question + resolve_question( + post_child.question, + "yes", + actual_resolve_time=make_aware(datetime(2024, 11, 1)), + ) + resolve_question( + post_parent.question, + "no", + actual_resolve_time=make_aware(datetime(2024, 11, 1)), + ) + post_conditional.refresh_from_db() + + assert post_conditional.resolved + assert post_conditional.actual_close_time == make_aware(datetime(2024, 11, 1)) + + assert post_conditional.conditional.question_no.resolution == "yes" + assert post_conditional.conditional.question_no.actual_close_time == make_aware( + datetime(2024, 11, 1) + ) + assert ( + post_conditional.conditional.question_no.actual_resolve_time + == make_aware(datetime(2024, 11, 1)) + ) + + assert post_conditional.conditional.question_yes.resolution == "annulled" + assert ( + post_conditional.conditional.question_yes.actual_close_time + == make_aware(datetime(2024, 11, 1)) + ) + assert ( + post_conditional.conditional.question_yes.actual_resolve_time + == make_aware(datetime(2024, 11, 1)) + ) + + def test_conditional_child_and_parent_resolved__unresolve( + self, post_conditional, post_child, post_parent + ): + """ + Unresolving conditional post + """ + + assert not post_conditional.resolved + + # Close child question + resolve_question( + post_child.question, + "yes", + actual_resolve_time=make_aware(datetime(2024, 11, 1)), + ) + resolve_question( + post_parent.question, + "no", + actual_resolve_time=make_aware(datetime(2024, 11, 1)), + ) + post_conditional.refresh_from_db() + assert post_conditional.resolved + assert post_conditional.actual_close_time + assert post_conditional.conditional.question_no.resolution == "yes" + assert post_conditional.conditional.question_yes.resolution == "annulled" + + # Unresolve action + unresolve_question(post_child.question) + post_conditional.refresh_from_db() + + assert not post_conditional.resolved + assert post_conditional.actual_close_time + assert not post_conditional.conditional.question_no.resolution + # For Luke: shouldn't other branch resolution become None instead of being annulled? + assert post_conditional.conditional.question_yes.resolution == "annulled" + + def test_conditional_parent_unresolve( + self, post_conditional, post_child, post_parent + ): + """ + Unresolved conditions + """ + + assert not post_conditional.resolved + + # Resolve condition + resolve_question( + post_parent.question, + "no", + actual_resolve_time=make_aware(datetime(2024, 11, 1)), + ) + post_conditional.refresh_from_db() + + assert not post_conditional.resolved + assert post_conditional.actual_close_time + + assert not post_conditional.conditional.question_no.resolution + assert post_conditional.conditional.question_no.actual_close_time == make_aware( + datetime(2024, 11, 1) + ) + assert not post_conditional.conditional.question_no.actual_resolve_time + + assert post_conditional.conditional.question_yes.resolution == "annulled" + assert ( + post_conditional.conditional.question_yes.actual_close_time + == make_aware(datetime(2024, 11, 1)) + ) + assert ( + post_conditional.conditional.question_yes.actual_resolve_time + == make_aware(datetime(2024, 11, 1)) + ) + + def test_conditional_child_resolved(self, post_conditional, post_child): + """ + Condition not resolved, Child resolved + """ + + assert not post_conditional.resolved + + # Resolve condition + resolve_question( + post_child.question, + "no", + actual_resolve_time=make_aware(datetime(2024, 11, 1)), + ) + post_conditional.refresh_from_db() + + assert not post_conditional.resolved + assert post_conditional.actual_close_time == make_aware(datetime(2024, 11, 1)) + + assert not post_conditional.conditional.question_yes.resolution + assert ( + post_conditional.conditional.question_yes.actual_close_time + == make_aware(datetime(2024, 11, 1)) + ) + assert not post_conditional.conditional.question_yes.actual_resolve_time + + assert not post_conditional.conditional.question_no.resolution + assert post_conditional.conditional.question_no.actual_close_time == make_aware( + datetime(2024, 11, 1) + ) + assert not post_conditional.conditional.question_no.actual_resolve_time