From f4236771751d528ec4f14cfb4dffb150b1cbba7c Mon Sep 17 00:00:00 2001 From: happylittle7 Date: Mon, 29 Dec 2025 02:45:46 +0800 Subject: [PATCH] feat: implement quota tracking and management in submission process --- problems/apps.py | 4 +++ problems/signals.py | 62 +++++++++++++++++++++++++++++++++++ submissions/admin.py | 4 +-- submissions/views.py | 78 ++++++++++++++++++++++++++++++-------------- 4 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 problems/signals.py diff --git a/problems/apps.py b/problems/apps.py index ac489e43..5928797c 100644 --- a/problems/apps.py +++ b/problems/apps.py @@ -4,3 +4,7 @@ class ProblemsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'problems' + + def ready(self): + # 導入 signals 以註冊 signal handlers + import problems.signals # noqa: F401 diff --git a/problems/signals.py b/problems/signals.py new file mode 100644 index 00000000..50e80319 --- /dev/null +++ b/problems/signals.py @@ -0,0 +1,62 @@ +""" +Problems app signals - 處理題目相關的 signal 事件 +""" +import logging +from django.db.models.signals import pre_save, post_save +from django.dispatch import receiver + +logger = logging.getLogger(__name__) + + +@receiver(pre_save, sender='problems.Problems') +def track_quota_change(sender, instance, **kwargs): + """ + 在保存前追蹤 total_quota 是否變更 + 將舊值存入 instance._old_total_quota + """ + if instance.pk: + try: + old_instance = sender.objects.get(pk=instance.pk) + instance._old_total_quota = old_instance.total_quota + except sender.DoesNotExist: + instance._old_total_quota = None + else: + instance._old_total_quota = None + + +@receiver(post_save, sender='problems.Problems') +def reset_user_quotas_on_change(sender, instance, created, **kwargs): + """ + 當題目的 total_quota 被修改時,重置所有該題目的 UserProblemQuota 記錄 + + 邏輯: + - 如果是新建題目,不需要處理 + - 如果 total_quota 沒有變更,不需要處理 + - 如果 total_quota 變更了,重置所有相關的 UserProblemQuota + """ + if created: + return + + old_quota = getattr(instance, '_old_total_quota', None) + new_quota = instance.total_quota + + # 如果 quota 沒有變更,不需要處理 + if old_quota == new_quota: + return + + # 延遲導入避免循環依賴 + from submissions.models import UserProblemQuota + + # 重置所有該題目的配額記錄 + updated_count = UserProblemQuota.objects.filter( + problem_id=instance.id, + assignment_id__isnull=True # 只重置全域配額,不影響作業配額 + ).update( + total_quota=new_quota, + remaining_attempts=new_quota + ) + + logger.info( + f"Problem {instance.id} total_quota changed from {old_quota} to {new_quota}. " + f"Reset {updated_count} user quota records." + ) diff --git a/submissions/admin.py b/submissions/admin.py index a5d88473..964c4cf0 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -236,8 +236,8 @@ def quota_usage(self, obj): percentage = (used / obj.total_quota * 100) if obj.total_quota > 0 else 0 color = 'green' if percentage < 50 else 'orange' if percentage < 80 else 'red' return format_html( - '{}/{} ({:.0f}%)', - color, used, obj.total_quota, percentage + '{}/{} ({}%)', + color, used, obj.total_quota, int(percentage) ) quota_usage.short_description = '配額使用' diff --git a/submissions/views.py b/submissions/views.py index e48aba2c..f03b800d 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -837,31 +837,9 @@ def post(self, request, *args, **kwargs): status_code=status.HTTP_403_FORBIDDEN ) - # 3. Quota 檢查 - 檢查用戶是否還有提交配額 - from .models import UserProblemQuota - try: - with transaction.atomic(): - quota = UserProblemQuota.objects.select_for_update().get( - user=user, - problem_id=problem_id, - assignment_id__isnull=True # 全域配額 - ) - if quota.remaining_attempts == 0: - return api_response( - data=None, - message="you have used all your quotas", - status_code=status.HTTP_403_FORBIDDEN - ) - # 減少配額(如果不是無限制) - if quota.remaining_attempts > 0: - quota.remaining_attempts -= 1 - quota.save() - except UserProblemQuota.DoesNotExist: - # 沒有配額限制,允許提交 - pass - - # 4. 題目權限檢查 + # 3. 題目權限檢查(先檢查題目是否存在,並獲取 quota 設定) from problems.models import Problems + from .models import UserProblemQuota try: problem = Problems.objects.get(id=problem_id) @@ -902,6 +880,58 @@ def post(self, request, *args, **kwargs): status_code=status.HTTP_404_NOT_FOUND ) + # 4. Quota 檢查 - 基於題目的 total_quota 設定 + problem_quota = problem.total_quota # -1 = 無限制, >= 0 = 有限制 + + if problem_quota >= 0: + # 題目有配額限制,需要檢查/建立該用戶的 UserProblemQuota 記錄 + with transaction.atomic(): + quota, created = UserProblemQuota.objects.select_for_update().get_or_create( + user=user, + problem_id=problem_id, + assignment_id=None, # 全域配額(非作業) + defaults={ + 'total_quota': problem_quota, + 'remaining_attempts': problem_quota + } + ) + + if quota.remaining_attempts == 0: + return api_response( + data=None, + message="you have used all your quotas", + status_code=status.HTTP_403_FORBIDDEN + ) + + # 減少配額 + if quota.remaining_attempts > 0: + quota.remaining_attempts -= 1 + quota.save() + else: + # 題目無配額限制 (total_quota == -1) + # 但仍檢查是否有管理員手動設定的 UserProblemQuota(針對特定用戶的限制) + try: + with transaction.atomic(): + quota = UserProblemQuota.objects.select_for_update().get( + user=user, + problem_id=problem_id, + assignment_id__isnull=True + ) + # 只有當 total_quota >= 0 時才檢查(-1 表示此用戶也無限制) + if quota.total_quota >= 0: + if quota.remaining_attempts == 0: + return api_response( + data=None, + message="you have used all your quotas", + status_code=status.HTTP_403_FORBIDDEN + ) + if quota.remaining_attempts > 0: + quota.remaining_attempts -= 1 + quota.save() + except UserProblemQuota.DoesNotExist: + # 沒有任何配額限制,允許提交 + pass + submission = serializer.save() # NOJ 格式響應