Skip to content
Closed
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
96 changes: 95 additions & 1 deletion planner/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2345,4 +2345,98 @@ 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)

class DailyPlanFinalizeViewTests(TestCase):
def setUp(self):
from django.contrib.auth import get_user_model
from exams.models import ExamPeriod, Exam, StudyTask

User = get_user_model()
self.user = User.objects.create_user(
username="finalize_view_tester", email="finalizeview@example.com", password="pass1234"
)
self.today = django_timezone.localdate()
self.exam_period = ExamPeriod.objects.create(
user=self.user, title="테스트 시험기간",
start_date=self.today, end_date=self.today + timedelta(days=10),
status="active",
)
self.exam = Exam.objects.create(
exam_period=self.exam_period, subject_name="테스트 과목",
exam_date=self.today + timedelta(days=5),
)
self.task = StudyTask.objects.create(
exam=self.exam, title="작업", importance="high", depth="core",
task_type="concept", difficulty="normal", order=1,
estimated_min_minutes=20, estimated_max_minutes=40, is_confirmed=True,
)
self.daily_plan = DailyPlan.objects.create(
exam_period=self.exam_period, date=self.today,
available_minutes=60, planned_minutes=40,
)
self.item = DailyPlanItem.objects.create(
daily_plan=self.daily_plan, study_task=self.task, planned_minutes=40, order=1,
)
self.client.login(username="finalizeview@example.com", password="pass1234")

def _finalize(self):
return self.client.post(reverse('planner:daily_plan_finalize'))

def test_finalize_without_unfinished_tasks_no_recovery(self):
record_progress(daily_plan_item=self.item, status="done", actual_minutes=40)

response = self._finalize()
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertFalse(data['needs_recovery'])
self.assertIsNone(data['recovery_group_id'])

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

def test_finalize_with_unfinished_task_creates_recovery(self):
AvailableTime.objects.create(
exam_period=self.exam_period, date=self.today + timedelta(days=1), available_minutes=60,
)
# 진행 기록 없이 마감 -> mark_unrecorded_as_not_done=True로 자동 못함 처리됨

response = self._finalize()
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertTrue(data['needs_recovery'])
self.assertIsNotNone(data['recovery_group_id'])

def test_finalize_no_plan_today_returns_404(self):
self.daily_plan.delete()
response = self._finalize()
self.assertEqual(response.status_code, 404)

def test_finalize_twice_returns_409(self):
record_progress(daily_plan_item=self.item, status="done", actual_minutes=40)
self._finalize()

response = self._finalize()
self.assertEqual(response.status_code, 409)

def test_finalize_future_plan_rejected(self):
future_plan = DailyPlan.objects.create(
exam_period=self.exam_period, date=self.today + timedelta(days=1),
available_minutes=60, planned_minutes=0,
)
# today_date 기준 조회라 future_plan은 애초에 조회 안 됨 -> 404
self.daily_plan.delete()

response = self._finalize()
self.assertEqual(response.status_code, 404)

def test_finalize_does_not_show_other_user_plan(self):
from django.contrib.auth import get_user_model
User = get_user_model()
other_user = User.objects.create_user(
username="other_finalize_view", email="other_finalizeview@example.com", password="pass1234"
)
self.client.login(username="other_finalizeview@example.com", password="pass1234")

response = self._finalize()
self.assertEqual(response.status_code, 404)
1 change: 1 addition & 0 deletions planner/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
path('today/', views.today, name='today'),
path('', views.dashboard, name='dashboard'),
path('progress/<int:item_id>/', views.progress_record, name='progress_record'),
path('daily-plans/finalize/', views.daily_plan_finalize, name='daily_plan_finalize'),
]
53 changes: 53 additions & 0 deletions planner/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
from django.urls import reverse
from exams.models import ExamPeriod, StudyTask, AvailableTime
from planner.services.feasibility_checker import calculate_feasibility, POSSIBLE
from planner.services.progress_recorder import (
finalize_daily_plan,
DailyPlanAlreadyFinalizedError,
FutureDailyPlanFinalizeError,
IncompleteProgressError,
)
from planner.services.schedule_generator import (
generate_schedule,
ScheduleAlreadyExistsError,
Expand Down Expand Up @@ -447,4 +453,51 @@ def progress_record(request, item_id):
"actual_minutes": progress_log.actual_minutes,
"completion_percent": progress_log.completion_percent,
"daily_plan_status": result['daily_plan_status'],
})

@login_required
@require_http_methods(["POST"])
def daily_plan_finalize(request):
today_date = timezone.localdate()

daily_plan = (
DailyPlan.objects
.filter(
exam_period__user=request.user,
exam_period__status=ExamPeriodStatus.ACTIVE,
date=today_date,
)
.first()
)

if daily_plan is None:
return JsonResponse(
{"message": "오늘 마감할 계획을 찾을 수 없습니다."}, status=404,
)

try:
result = finalize_daily_plan(daily_plan, mark_unrecorded_as_not_done=True)
except DailyPlanAlreadyFinalizedError as exc:
return JsonResponse({"message": str(exc)}, status=409)
except FutureDailyPlanFinalizeError as exc:
return JsonResponse({"message": str(exc)}, status=400)
except IncompleteProgressError as exc:
return JsonResponse({"message": str(exc)}, status=400)

recovery_plans = result.get('recovery_plans')
recovery_group_id = None
if recovery_plans:
candidate = recovery_plans.get('maintain_volume') or recovery_plans.get('core_focus')
if candidate:
recovery_group_id = str(candidate.recovery_group_id)

if result['needs_recovery'] and recovery_group_id:
redirect_url = "#" # TODO: recovery_compare View 생기면 교체
else:
redirect_url = reverse('planner:dashboard')

return JsonResponse({
"needs_recovery": result['needs_recovery'],
"recovery_group_id": recovery_group_id,
"redirect_url": redirect_url,
})