Skip to content
Open
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
328 changes: 327 additions & 1 deletion planner/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2217,6 +2217,332 @@ def test_today_eod_includes_pending_as_not_done(self):
self.assertEqual(response.context['eod']['pending_count'], 1)
self.assertEqual(response.context['eod']['not_done_count'], 1)

class DailyPlanFinalizeViewTests(TestCase):
"""
#79 daily_plan_finalize API 테스트.

로그인한 사용자의 ACTIVE 시험기간에 속한 오늘 DailyPlan을 조회하고,
finalize_daily_plan()을 호출해 하루 계획을 마감하는 JSON API를 검증한다.
"""

def setUp(self):
from django.contrib.auth import get_user_model

User = get_user_model()

self.user = User.objects.create_user(
username="finalize_view_tester",
email="finalize_view@example.com",
password="pass1234",
)
self.today = django_timezone.localdate()

self.client.login(
username="finalize_view@example.com",
password="pass1234",
)
self.url = reverse("planner:daily_plan_finalize")

def _make_active_exam_period(self, user=None, title="마감 테스트 시험기간"):
from exams.models import ExamPeriod

return ExamPeriod.objects.create(
user=user or self.user,
title=title,
start_date=self.today - timedelta(days=1),
end_date=self.today + timedelta(days=10),
status="active",
)

def _make_exam(self, exam_period, exam_date=None):
from exams.models import Exam

return Exam.objects.create(
exam_period=exam_period,
subject_name="테스트 과목",
exam_date=exam_date or self.today + timedelta(days=5),
)

def _make_task(self, exam, importance="high", depth="core"):
from exams.models import StudyTask

return StudyTask.objects.create(
exam=exam,
title="마감 대상 작업",
importance=importance,
depth=depth,
task_type="concept",
difficulty="normal",
order=1,
estimated_min_minutes=20,
estimated_max_minutes=40,
is_confirmed=True,
)

def _make_today_plan_with_item(
self,
exam_period,
importance="high",
depth="core",
):
exam = self._make_exam(exam_period)
task = self._make_task(
exam,
importance=importance,
depth=depth,
)

daily_plan = DailyPlan.objects.create(
exam_period=exam_period,
date=self.today,
available_minutes=40,
planned_minutes=40,
)
item = DailyPlanItem.objects.create(
daily_plan=daily_plan,
study_task=task,
planned_minutes=40,
order=1,
)

return daily_plan, item

def test_no_active_exam_period_returns_404(self):
response = self.client.post(self.url)

self.assertEqual(response.status_code, 404)
self.assertEqual(
response.json()["message"],
"오늘 마감할 계획이 없습니다.",
)

def test_no_today_plan_returns_404(self):
self._make_active_exam_period()

response = self.client.post(self.url)

self.assertEqual(response.status_code, 404)
self.assertEqual(
response.json()["message"],
"오늘 마감할 계획이 없습니다.",
)

def test_other_users_today_plan_returns_404(self):
from django.contrib.auth import get_user_model

User = get_user_model()

other_user = User.objects.create_user(
username="other_finalize_tester",
email="other_finalize@example.com",
password="pass1234",
)
other_period = self._make_active_exam_period(user=other_user)
self._make_today_plan_with_item(other_period)

response = self.client.post(self.url)

self.assertEqual(response.status_code, 404)
self.assertEqual(
response.json()["message"],
"오늘 마감할 계획이 없습니다.",
)

def test_all_done_finalize_needs_recovery_false(self):
exam_period = self._make_active_exam_period()
daily_plan, item = self._make_today_plan_with_item(exam_period)

record_progress(
daily_plan_item=item,
status="done",
actual_minutes=35,
)

response = self.client.post(self.url)
body = response.json()

self.assertEqual(response.status_code, 200)
self.assertFalse(body["needs_recovery"])
self.assertFalse(body["recovery_available"])
self.assertIsNone(body["recovery_group_id"])
self.assertEqual(body["auto_marked_not_done_count"], 0)

daily_plan.refresh_from_db()
self.assertIsNotNone(daily_plan.finalized_at)

def test_partial_or_not_done_needs_recovery_true(self):
exam_period = self._make_active_exam_period()
_daily_plan, item = self._make_today_plan_with_item(
exam_period,
importance="low",
depth="optional",
)

record_progress(
daily_plan_item=item,
status="not_done",
actual_minutes=0,
)
AvailableTime.objects.create(
exam_period=exam_period,
date=self.today + timedelta(days=1),
available_minutes=60,
)

response = self.client.post(self.url)
body = response.json()

self.assertEqual(response.status_code, 200)
self.assertTrue(body["needs_recovery"])

def test_unrecorded_task_auto_marked_not_done(self):
exam_period = self._make_active_exam_period()
_daily_plan, item = self._make_today_plan_with_item(
exam_period,
importance="low",
depth="optional",
)
AvailableTime.objects.create(
exam_period=exam_period,
date=self.today + timedelta(days=1),
available_minutes=60,
)

response = self.client.post(self.url)
body = response.json()

self.assertEqual(response.status_code, 200)
self.assertEqual(body["auto_marked_not_done_count"], 1)
self.assertTrue(body["needs_recovery"])

item.refresh_from_db()
self.assertEqual(
item.progress_log.progress_status,
"not_done",
)

def test_recovery_available_true_returns_group_id(self):
exam_period = self._make_active_exam_period()
_daily_plan, item = self._make_today_plan_with_item(
exam_period,
importance="low",
depth="optional",
)

record_progress(
daily_plan_item=item,
status="not_done",
actual_minutes=0,
)
AvailableTime.objects.create(
exam_period=exam_period,
date=self.today + timedelta(days=1),
available_minutes=60,
)

response = self.client.post(self.url)
body = response.json()

self.assertEqual(response.status_code, 200)
self.assertTrue(body["recovery_available"])
self.assertIsNotNone(body["recovery_group_id"])

self.assertTrue(
RecoveryPlan.objects.filter(
recovery_group_id=body["recovery_group_id"],
).exists()
)

def test_recovery_unavailable_when_both_types_fail(self):
exam_period = self._make_active_exam_period()

# high/core 작업은 핵심 집중형 제외 후보가 아니다.
# 미래 가용시간도 없으므로 분량 유지형도 생성되지 않는다.
_daily_plan, item = self._make_today_plan_with_item(
exam_period,
importance="high",
depth="core",
)

record_progress(
daily_plan_item=item,
status="not_done",
actual_minutes=0,
)

response = self.client.post(self.url)
body = response.json()

self.assertEqual(response.status_code, 200)
self.assertTrue(body["needs_recovery"])
self.assertFalse(body["recovery_available"])
self.assertIsNone(body["recovery_group_id"])

def test_already_finalized_returns_409(self):
exam_period = self._make_active_exam_period()
_daily_plan, item = self._make_today_plan_with_item(exam_period)

record_progress(
daily_plan_item=item,
status="done",
actual_minutes=35,
)

first_response = self.client.post(self.url)
self.assertEqual(first_response.status_code, 200)

second_response = self.client.post(self.url)

self.assertEqual(second_response.status_code, 409)
self.assertEqual(
second_response.json()["message"],
"이미 마감된 계획입니다.",
)

def test_get_method_not_allowed(self):
exam_period = self._make_active_exam_period()
self._make_today_plan_with_item(exam_period)

response = self.client.get(self.url)

self.assertEqual(response.status_code, 405)

def test_finds_today_plan_in_older_active_exam_period(self):
"""
더 최근에 생성된 ACTIVE 시험기간에는 오늘 계획이 없고,
이전 ACTIVE 시험기간에만 오늘 계획이 있어도 정상 마감해야 한다.
"""
from exams.models import ExamPeriod

older_period = self._make_active_exam_period(
title="이전 시험기간",
)
daily_plan, item = self._make_today_plan_with_item(older_period)

newer_period = self._make_active_exam_period(
title="최근 시험기간",
)

# 생성 시각의 우선순위를 명확하게 만든다.
ExamPeriod.objects.filter(pk=older_period.pk).update(
created_at=django_timezone.now() - timedelta(minutes=1),
)
ExamPeriod.objects.filter(pk=newer_period.pk).update(
created_at=django_timezone.now(),
)

record_progress(
daily_plan_item=item,
status="done",
actual_minutes=35,
)

response = self.client.post(self.url)

self.assertEqual(response.status_code, 200)

daily_plan.refresh_from_db()
self.assertIsNotNone(daily_plan.finalized_at)

class ProgressRecordViewTests(TestCase):
def setUp(self):
Expand Down Expand Up @@ -2345,4 +2671,4 @@ def test_rejects_edit_after_finalized(self):
def test_speed_factor_updated_after_record(self):
self._post(self.item.id, {"status": "done", "actual_minutes": 60})
self.exam.refresh_from_db()
self.assertNotEqual(self.exam.speed_factor, 1.0)
self.assertNotEqual(self.exam.speed_factor, 1.0)
1 change: 1 addition & 0 deletions planner/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
path('plan/generate/<int:period_id>/', views.plan_generate, name='plan_generate'),
path('plan/complete/<int:period_id>/', views.plan_complete, name='plan_complete'),
path('today/', views.today, name='today'),
path('daily-plans/finalize/', views.daily_plan_finalize, name='daily_plan_finalize'),
path('', views.dashboard, name='dashboard'),
path('progress/<int:item_id>/', views.progress_record, name='progress_record'),
]
Loading