From 3e796deaa21119eb405a3b8b385ad4d636e32488 Mon Sep 17 00:00:00 2001 From: int-y1 Date: Sun, 2 Mar 2025 04:01:12 -0500 Subject: [PATCH] Add support for archiving a submission --- judge/admin/submission.py | 6 ++-- .../migrations/0150_submission_is_archived.py | 35 +++++++++++++++++++ judge/models/problem.py | 7 ++-- judge/models/profile.py | 7 ++-- judge/models/submission.py | 5 +++ judge/models/tests/test_problem.py | 6 ++-- judge/performance_points.py | 2 ++ judge/signals.py | 10 ++++++ judge/tasks/submission.py | 12 ++++--- judge/utils/problems.py | 3 +- judge/views/problem.py | 3 +- judge/views/problem_manage.py | 15 ++++---- judge/views/ranked_submission.py | 4 +-- judge/views/user.py | 2 +- resources/submission.scss | 8 +++++ templates/problem/manage_submission.html | 6 ++++ templates/submission/list.html | 2 +- templates/submission/row.html | 3 ++ 18 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 judge/migrations/0150_submission_is_archived.py diff --git a/judge/admin/submission.py b/judge/admin/submission.py index 124853365a..f73e4b2f87 100644 --- a/judge/admin/submission.py +++ b/judge/admin/submission.py @@ -120,8 +120,8 @@ def get_formset(self, request, obj=None, **kwargs): class SubmissionAdmin(VersionAdmin): readonly_fields = ('user', 'problem', 'date', 'judged_date') - fields = ('user', 'problem', 'date', 'judged_date', 'locked_after', 'time', 'memory', 'points', 'language', - 'status', 'result', 'case_points', 'case_total', 'judged_on', 'error') + fields = ('user', 'problem', 'date', 'judged_date', 'locked_after', 'is_archived', 'time', 'memory', 'points', + 'language', 'status', 'result', 'case_points', 'case_total', 'judged_on', 'error') actions = ('judge', 'recalculate_score') list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory', 'points', 'language_column', 'status', 'result', 'judge_column') @@ -135,6 +135,8 @@ def get_readonly_fields(self, request, obj=None): fields = self.readonly_fields if not request.user.has_perm('judge.lock_submission'): fields += ('locked_after',) + if not request.user.has_perm('judge.archive_submission'): + fields += ('is_archived',) return fields def get_queryset(self, request): diff --git a/judge/migrations/0150_submission_is_archived.py b/judge/migrations/0150_submission_is_archived.py new file mode 100644 index 0000000000..004b2cce14 --- /dev/null +++ b/judge/migrations/0150_submission_is_archived.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.17 on 2025-03-02 10:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0149_add_organization_private_problems_permission'), + ] + + operations = [ + migrations.AlterModelOptions( + name='submission', + options={ + 'permissions': ( + ('abort_any_submission', 'Abort any submission'), + ('rejudge_submission', 'Rejudge the submission'), + ('rejudge_submission_lot', 'Rejudge a lot of submissions'), + ('spam_submission', 'Submit without limit'), + ('view_all_submission', 'View all submission'), + ('resubmit_other', "Resubmit others' submission"), + ('lock_submission', 'Change lock status of submission'), + ('archive_submission', 'Archive any submission'), + ), + 'verbose_name': 'submission', + 'verbose_name_plural': 'submissions', + }, + ), + migrations.AddField( + model_name='submission', + name='is_archived', + field=models.BooleanField(default=False, verbose_name='is archived'), + ), + ] diff --git a/judge/models/problem.py b/judge/models/problem.py index 50f30f5cdb..e46d003c75 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -394,8 +394,8 @@ def submission_source_visibility(self): return self.submission_source_visibility_mode def update_stats(self): - all_queryset = self.submission_set.filter(user__is_unlisted=False) - ac_queryset = all_queryset.filter(points__gte=self.points, result='AC') + all_queryset = self.submission_set.filter(user__is_unlisted=False, is_archived=False) + ac_queryset = all_queryset.filter(result='AC', case_points__gte=F('case_total')) self.user_count = ac_queryset.values('user').distinct().count() submissions = all_queryset.count() if submissions: @@ -466,7 +466,8 @@ def save(self, *args, **kwargs): def is_solved_by(self, user): # Return true if a full AC submission to the problem from the user exists. - return self.submission_set.filter(user=user.profile, result='AC', points__gte=F('problem__points')).exists() + return self.submission_set.filter( + user=user.profile, is_archived=False, result='AC', case_points__gte=F('case_total')).exists() def vote_permission_for_user(self, user): if not user.is_authenticated: diff --git a/judge/models/profile.py b/judge/models/profile.py index 859c30d47f..de396a35b7 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -224,7 +224,7 @@ def display_name(self): @cached_property def has_any_solves(self): - return self.submission_set.filter(result='AC', case_points__gte=F('case_total')).exists() + return self.submission_set.filter(is_archived=False, result='AC', case_points__gte=F('case_total')).exists() @cached_property def resolved_ace_theme(self): @@ -243,7 +243,8 @@ def calculate_points(self, table=_pp_table): from judge.models import Problem public_problems = Problem.get_public_problems() data = ( - public_problems.filter(submission__user=self, submission__points__isnull=False) + public_problems.filter(submission__user=self, submission__is_archived=False, + submission__points__isnull=False) .annotate(max_points=Max('submission__points')).order_by('-max_points') .values_list('max_points', flat=True).filter(max_points__gt=0) ) @@ -251,7 +252,7 @@ def calculate_points(self, table=_pp_table): points = sum(data) entries = min(len(data), len(table)) problems = ( - public_problems.filter(submission__user=self, submission__result='AC', + public_problems.filter(submission__user=self, submission__is_archived=False, submission__result='AC', submission__case_points__gte=F('submission__case_total')) .values('id').distinct().count() ) diff --git a/judge/models/submission.py b/judge/models/submission.py index 5aa6c67659..ee55822cc5 100644 --- a/judge/models/submission.py +++ b/judge/models/submission.py @@ -88,6 +88,7 @@ class Submission(models.Model): contest_object = models.ForeignKey('Contest', verbose_name=_('contest'), null=True, blank=True, on_delete=models.SET_NULL, related_name='+', db_index=False) locked_after = models.DateTimeField(verbose_name=_('submission lock'), null=True, blank=True) + is_archived = models.BooleanField(verbose_name=_('is archived'), default=False) @classmethod def result_class_from_code(cls, result, case_points, case_total): @@ -129,6 +130,9 @@ def judge(self, *args, rejudge=False, force_judge=False, rejudge_user=None, **kw revisions.set_comment('Rejudged') revisions.add_to_revision(self) judge_submission(self, *args, rejudge=rejudge, **kwargs) + elif not self.is_archived: + self.is_archived = True + self.save(update_fields=['is_archived']) judge.alters_data = True @@ -227,6 +231,7 @@ class Meta: ('view_all_submission', _('View all submission')), ('resubmit_other', _("Resubmit others' submission")), ('lock_submission', _('Change lock status of submission')), + ('archive_submission', _('Archive any submission')), ) verbose_name = _('submission') verbose_name_plural = _('submissions') diff --git a/judge/models/tests/test_problem.py b/judge/models/tests/test_problem.py index 05b636955d..6440a6d769 100644 --- a/judge/models/tests/test_problem.py +++ b/judge/models/tests/test_problem.py @@ -251,6 +251,8 @@ def give_basic_problem_ac(self, user, points=None): problem=self.basic_problem, result='AC', points=self.basic_problem.points if points is None else points, + case_points=(self.basic_problem.points if points is None else points) * 100, + case_total=self.basic_problem.points * 100, language=Language.get_python3(), ) @@ -294,8 +296,8 @@ def test_problem_voting_permissions(self): self.assertEqual(self.basic_problem.vote_permission_for_user(self.users['normal']), VotePermission.VOTE) partial_ac = create_user(username='partial_ac') - self.give_basic_problem_ac(partial_ac, 0.5) # ensure this value is not equal to its point value - self.assertNotEqual(self.basic_problem.points, 0.5) + self.give_basic_problem_ac(partial_ac, 0.5) # ensure this value is less than its point value + self.assertLess(0.5, self.basic_problem.points) self.assertEqual(self.basic_problem.vote_permission_for_user(partial_ac), VotePermission.VIEW) def test_problems_list(self): diff --git a/judge/performance_points.py b/judge/performance_points.py index 30de63e19d..a1eaf7666e 100644 --- a/judge/performance_points.py +++ b/judge/performance_points.py @@ -37,6 +37,7 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): INNER JOIN judge_submission ON (judge_problem.id = judge_submission.problem_id) WHERE (judge_problem.is_public AND NOT judge_problem.is_organization_private AND + NOT judge_submission.is_archived AND judge_submission.points IS NOT NULL AND judge_submission.user_id = %s) GROUP BY judge_problem.id @@ -44,6 +45,7 @@ def get_pp_breakdown(user, start=0, end=settings.DMOJ_PP_ENTRIES): ) AS max_points_table {join_type} judge_submission ON ( judge_submission.problem_id = max_points_table.problem_id AND + NOT judge_submission.is_archived AND judge_submission.points = max_points_table.max_points AND judge_submission.user_id = %s ) diff --git a/judge/signals.py b/judge/signals.py index c54554b46b..18461af471 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -127,6 +127,16 @@ def submission_delete(sender, instance, **kwargs): instance.problem.update_stats() +@receiver(post_save, sender=Submission) +def submission_update(sender, instance, update_fields, **kwargs): + if update_fields and 'is_archived' in update_fields: + finished_submission(instance) + instance.user._updating_stats_only = True + instance.user.calculate_points() + instance.problem._updating_stats_only = True + instance.problem.update_stats() + + @receiver(post_delete, sender=ContestSubmission) def contest_submission_delete(sender, instance, **kwargs): participation = instance.participation diff --git a/judge/tasks/submission.py b/judge/tasks/submission.py index a190ba71ee..dc3daa8b75 100644 --- a/judge/tasks/submission.py +++ b/judge/tasks/submission.py @@ -10,7 +10,7 @@ __all__ = ('apply_submission_filter', 'rejudge_problem_filter', 'rescore_problem') -def apply_submission_filter(queryset, id_range, languages, results): +def apply_submission_filter(queryset, id_range, languages, results, archive_locked): if id_range: start, end = id_range queryset = queryset.filter(id__gte=start, id__lte=end) @@ -18,15 +18,17 @@ def apply_submission_filter(queryset, id_range, languages, results): queryset = queryset.filter(language_id__in=languages) if results: queryset = queryset.filter(result__in=results) - queryset = queryset.exclude(locked_after__lt=timezone.now()) \ - .exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS) + if not archive_locked: + queryset = queryset.exclude(locked_after__lt=timezone.now()) + queryset = queryset.exclude(status__in=Submission.IN_PROGRESS_GRADING_STATUS) return queryset @shared_task(bind=True) -def rejudge_problem_filter(self, problem_id, id_range=None, languages=None, results=None, user_id=None): +def rejudge_problem_filter(self, problem_id, id_range=None, languages=None, results=None, archive_locked=None, + user_id=None): queryset = Submission.objects.filter(problem_id=problem_id) - queryset = apply_submission_filter(queryset, id_range, languages, results) + queryset = apply_submission_filter(queryset, id_range, languages, results, archive_locked) user = User.objects.get(id=user_id) rejudged = 0 diff --git a/judge/utils/problems.py b/judge/utils/problems.py index 44e7af4576..2a881a10c2 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -34,7 +34,8 @@ def user_completed_ids(profile): key = 'user_complete:%d' % profile.id result = cache.get(key) if result is None: - result = set(Submission.objects.filter(user=profile, result='AC', case_points__gte=F('case_total')) + result = set(Submission.objects + .filter(user=profile, is_archived=False, result='AC', case_points__gte=F('case_total')) .values_list('problem_id', flat=True).distinct()) cache.set(key, result, 86400) return result diff --git a/judge/views/problem.py b/judge/views/problem.py index 20be48b6da..dfd36bbc5a 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -450,7 +450,8 @@ def get_normal_queryset(self): queryset = Problem.objects.filter(filter).select_related('group').defer('description', 'summary') if self.profile is not None and self.hide_solved: queryset = queryset.exclude(id__in=Submission.objects - .filter(user=self.profile, result='AC', case_points__gte=F('case_total')) + .filter(user=self.profile, is_archived=False, + result='AC', case_points__gte=F('case_total')) .values_list('problem_id', flat=True)) if self.show_types: queryset = queryset.prefetch_related('types') diff --git a/judge/views/problem_manage.py b/judge/views/problem_manage.py index 5857b0cc55..b37e5818d0 100644 --- a/judge/views/problem_manage.py +++ b/judge/views/problem_manage.py @@ -82,15 +82,17 @@ def perform_action(self): except ValueError: return HttpResponseBadRequest() - return self.generate_response(id_range, languages, self.request.POST.getlist('result')) + archive_locked = self.request.POST.get('archive_locked', 'off') == 'on' - def generate_response(self, id_range, languages, results): + return self.generate_response(id_range, languages, self.request.POST.getlist('result'), archive_locked) + + def generate_response(self, id_range, languages, results, archive_locked): raise NotImplementedError() class RejudgeSubmissionsView(BaseRejudgeSubmissionsView): - def generate_response(self, id_range, languages, results): - status = rejudge_problem_filter.delay(self.object.id, id_range, languages, results, + def generate_response(self, id_range, languages, results, archive_locked): + status = rejudge_problem_filter.delay(self.object.id, id_range, languages, results, archive_locked, user_id=self.request.user.id) return redirect_to_task_status( status, message=_('Rejudging selected submissions for %s...') % (self.object.name,), @@ -99,8 +101,9 @@ def generate_response(self, id_range, languages, results): class PreviewRejudgeSubmissionsView(BaseRejudgeSubmissionsView): - def generate_response(self, id_range, languages, results): - queryset = apply_submission_filter(self.object.submission_set.all(), id_range, languages, results) + def generate_response(self, id_range, languages, results, archive_locked): + queryset = apply_submission_filter(self.object.submission_set.all(), + id_range, languages, results, archive_locked) return HttpResponse(str(queryset.count())) diff --git a/judge/views/ranked_submission.py b/judge/views/ranked_submission.py index d2f9473fb7..9ddc42627b 100644 --- a/judge/views/ranked_submission.py +++ b/judge/views/ranked_submission.py @@ -42,12 +42,12 @@ def get_queryset(self): FROM ( SELECT sub.user_id AS uid, MAX(sub.points) AS points FROM judge_submission AS sub {contest_join} - WHERE sub.problem_id = %s AND {points} > 0 {constraint} + WHERE sub.problem_id = %s AND NOT sub.is_archived AND {points} > 0 {constraint} GROUP BY sub.user_id ) AS highscore STRAIGHT_JOIN ( SELECT sub.user_id AS uid, sub.points, MIN(sub.time) as time FROM judge_submission AS sub {contest_join} - WHERE sub.problem_id = %s AND {points} > 0 {constraint} + WHERE sub.problem_id = %s AND NOT sub.is_archived AND {points} > 0 {constraint} GROUP BY sub.user_id, {points} ) AS fastest ON (highscore.uid = fastest.uid AND highscore.points = fastest.points) STRAIGHT_JOIN judge_submission AS sub diff --git a/judge/views/user.py b/judge/views/user.py index e38b0cd5ec..1d97d155c0 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -203,7 +203,7 @@ class UserProblemsPage(UserPage): def get_context_data(self, **kwargs): context = super(UserProblemsPage, self).get_context_data(**kwargs) - result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True, + result = Submission.objects.filter(user=self.object, is_archived=False, points__gt=0, problem__is_public=True, problem__is_organization_private=False) \ .exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \ .values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \ diff --git a/resources/submission.scss b/resources/submission.scss index 72403946a6..abc8ec46cc 100644 --- a/resources/submission.scss +++ b/resources/submission.scss @@ -29,6 +29,14 @@ border-top: none; } + &.submission-archived { + opacity: 0.6; + + &:hover { + opacity: 1; + } + } + > div { padding: 7px 5px; vertical-align: middle; diff --git a/templates/problem/manage_submission.html b/templates/problem/manage_submission.html index 9cd3d7a385..54fd148747 100644 --- a/templates/problem/manage_submission.html +++ b/templates/problem/manage_submission.html @@ -148,6 +148,12 @@

{{ _('Rejudge Submissions') }}

{% endfor %} +
+ +
{{ _('Rejudge selected submissions') }} diff --git a/templates/submission/list.html b/templates/submission/list.html index c524562129..1125603520 100644 --- a/templates/submission/list.html +++ b/templates/submission/list.html @@ -355,7 +355,7 @@

{{ _('Statistics') }}

{% set profile_id = request.profile.id if request.user.is_authenticated else 0 %} {% for submission in submissions %} -
+
{% with problem_name=show_problem and (submission.problem.i18n_name or submission.problem.name) %} {% include "submission/row.html" %} {% endwith %} diff --git a/templates/submission/row.html b/templates/submission/row.html index fff8b93450..c4e65a2fcb 100644 --- a/templates/submission/row.html +++ b/templates/submission/row.html @@ -35,6 +35,9 @@ {% endif %} + {% if submission.is_archived %} + + {% endif %}