Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for archiving a submission #2411

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
6 changes: 4 additions & 2 deletions judge/admin/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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):
Expand Down
35 changes: 35 additions & 0 deletions judge/migrations/0150_submission_is_archived.py
Original file line number Diff line number Diff line change
@@ -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',
},
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, what does this part of the migration do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is needed so that the permission archive_submission appears under /admin/auth/user/<id>/change/ > Available user permissions

migrations.AddField(
model_name='submission',
name='is_archived',
field=models.BooleanField(default=False, verbose_name='is archived'),
),
]
7 changes: 4 additions & 3 deletions judge/models/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions judge/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -243,15 +243,16 @@ 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)
)
bonus_function = settings.DMOJ_PP_BONUS_FUNCTION
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()
)
Expand Down
5 changes: 5 additions & 0 deletions judge/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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')
Expand Down
6 changes: 4 additions & 2 deletions judge/models/tests/test_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)

Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions judge/performance_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ 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
HAVING MAX(judge_submission.points) > 0.0
) 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
)
Expand Down
10 changes: 10 additions & 0 deletions judge/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions judge/tasks/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@
__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)
if languages:
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
Expand Down
3 changes: 2 additions & 1 deletion judge/utils/problems.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion judge/views/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
15 changes: 9 additions & 6 deletions judge/views/problem_manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,),
Expand All @@ -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()))


Expand Down
4 changes: 2 additions & 2 deletions judge/views/ranked_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion judge/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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') \
Expand Down
8 changes: 8 additions & 0 deletions resources/submission.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
border-top: none;
}

&.submission-archived {
opacity: 0.6;

&:hover {
opacity: 1;
}
}

> div {
padding: 7px 5px;
vertical-align: middle;
Expand Down
6 changes: 6 additions & 0 deletions templates/problem/manage_submission.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ <h3>{{ _('Rejudge Submissions') }}</h3>
{% endfor %}
</select>
</div>
<div class="control-group">
<label>
{{ _('Archive locked submissions:') }}
<input id="by-archive-locked-check" type="checkbox" name="archive_locked" checked>
</label>
</div>
<a id="rejudge-selected" class="unselectable button full" href="#">
{{ _('Rejudge selected submissions') }}
</a>
Expand Down
2 changes: 1 addition & 1 deletion templates/submission/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ <h3>{{ _('Statistics') }} <i class="fa fa-pie-chart"></i></h3>
<div id="submissions-table">
{% set profile_id = request.profile.id if request.user.is_authenticated else 0 %}
{% for submission in submissions %}
<div class="submission-row" id="{{ submission.id }}">
<div class="submission-row{% if submission.is_archived %} submission-archived{% endif %}" id="{{ submission.id }}">
{% with problem_name=show_problem and (submission.problem.i18n_name or submission.problem.name) %}
{% include "submission/row.html" %}
{% endwith %}
Expand Down
3 changes: 3 additions & 0 deletions templates/submission/row.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
<i title="{{ submission.contest_object.name }}" class="fa fa-dot-circle-o"></i>
</a>
{% endif %}
{% if submission.is_archived %}
<i class="fa fa-archive"></i>
{% endif %}
</div>
</div>

Expand Down