From 8c661ac8373fde0320f22631a1da19cbebe42db0 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Mon, 16 Sep 2024 00:13:42 +0200 Subject: [PATCH] Allow changing oicompare format --- oioioi/contestexcl/tests.py | 1 + oioioi/problems/controllers.py | 4 ++ oioioi/programs/admin.py | 37 +++++++++++ oioioi/programs/handlers.py | 2 +- ...20_programsubmission_user_language_code.py | 18 ----- ...msubmission_user_language_code_and_more.py | 49 ++++++++++++++ oioioi/programs/models.py | 41 ++++++++++++ oioioi/programs/tests.py | 66 ++++++++++++++++++- oioioi/programs/utils.py | 15 +++++ 9 files changed, 213 insertions(+), 20 deletions(-) delete mode 100644 oioioi/programs/migrations/0020_programsubmission_user_language_code.py create mode 100644 oioioi/programs/migrations/0020_programsubmission_user_language_code_and_more.py diff --git a/oioioi/contestexcl/tests.py b/oioioi/contestexcl/tests.py index 1ac43b6b0..88367d72c 100644 --- a/oioioi/contestexcl/tests.py +++ b/oioioi/contestexcl/tests.py @@ -189,6 +189,7 @@ def _modify_contestexcl( ('contestlogo', 0, 0, 0, 1), ('programs_config', 0, 0, 0, 1), ('contestcompiler_set', 0, 0, 0, 1000), + ('checkerformatforcontest', 0, 0, 0, 1), ) data = dict() for (name, total, initial, min_num, max_num) in formsets: diff --git a/oioioi/problems/controllers.py b/oioioi/problems/controllers.py index b90a9f5c5..7eeb60345 100644 --- a/oioioi/problems/controllers.py +++ b/oioioi/problems/controllers.py @@ -24,6 +24,7 @@ from oioioi.evalmgr.tasks import create_environ, delay_environ from oioioi.problems.models import ProblemStatistics, UserStatistics from oioioi.problems.utils import can_admin_problem +from oioioi.programs.utils import get_checker_format logger = logging.getLogger(__name__) @@ -156,6 +157,9 @@ def judge(self, submission, extra_args=None, is_rejudge=False): break if user_lang: environ['user_language'] = user_lang + else: + environ['user_language'] = 'english' + environ['checker_format'] = environ['user_language'] + '_' + get_checker_format(submission.problem_instance) picontroller = submission.problem_instance.controller picontroller.fill_evaluation_environ(environ, submission) diff --git a/oioioi/programs/admin.py b/oioioi/programs/admin.py index 5ebd8b5a9..4132c982a 100644 --- a/oioioi/programs/admin.py +++ b/oioioi/programs/admin.py @@ -31,6 +31,8 @@ ProgramsConfig, ReportActionsConfig, Test, + CheckerFormatForContest, + CheckerFormatForProblem, ) @@ -447,3 +449,38 @@ def queryset(self, request, queryset): return queryset.filter(condition) else: return queryset + + +class CheckerFormatForContestInline(admin.StackedInline): + model = CheckerFormatForContest + category = _("Advanced") + + +class CheckerFormatForProblemInline(admin.StackedInline): + model = CheckerFormatForProblem + category = _("Advanced") + + +class CheckerFormatOverrideContestAdminMixin(object): + """Adds :class:`~oioioi.programs.models.CheckerFormatForContest` to an admin + panel. + """ + def __init__(self, *args, **kwargs): + super(CheckerFormatOverrideContestAdminMixin, self).__init__(*args, **kwargs) + self.inlines = tuple(self.inlines) + (CheckerFormatForContestInline,) + + +ContestAdmin.mix_in(CheckerFormatOverrideContestAdminMixin) + + +class CheckerFormatOverrideProblemAdminMixin(object): + """Adds :class:`~oioioi.programs.models.CheckerFormatForProblem` to an admin + panel. + """ + + def __init__(self, *args, **kwargs): + super(CheckerFormatOverrideProblemAdminMixin, self).__init__(*args, **kwargs) + self.inlines = tuple(self.inlines) + (CheckerFormatForProblemInline,) + + +ProblemInstanceAdmin.mix_in(CheckerFormatOverrideProblemAdminMixin) diff --git a/oioioi/programs/handlers.py b/oioioi/programs/handlers.py index 468903857..03bcee9d2 100755 --- a/oioioi/programs/handlers.py +++ b/oioioi/programs/handlers.py @@ -289,7 +289,7 @@ def run_tests(env, kind=None, **kwargs): job['check_output'] = env.get('check_outputs', True) if env.get('checker'): job['chk_file'] = env['checker'] - job['checker_language'] = env.get('user_language', 'polish') + job['checker_format'] = env.get('checker_format', 'english_abbreviated') if env.get('save_outputs'): job.setdefault('out_file', _make_filename(env, test_name + '.out')) job['upload_out'] = True diff --git a/oioioi/programs/migrations/0020_programsubmission_user_language_code.py b/oioioi/programs/migrations/0020_programsubmission_user_language_code.py deleted file mode 100644 index a07514efb..000000000 --- a/oioioi/programs/migrations/0020_programsubmission_user_language_code.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.16 on 2024-09-09 10:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('programs', '0019_add_limits_override'), - ] - - operations = [ - migrations.AddField( - model_name='programsubmission', - name='user_language_code', - field=models.CharField(blank=True, max_length=6, null=True, verbose_name='User language code'), - ), - ] diff --git a/oioioi/programs/migrations/0020_programsubmission_user_language_code_and_more.py b/oioioi/programs/migrations/0020_programsubmission_user_language_code_and_more.py new file mode 100644 index 000000000..7a0f84311 --- /dev/null +++ b/oioioi/programs/migrations/0020_programsubmission_user_language_code_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.16 on 2024-09-15 21:27 + +from django.db import migrations, models +import django.db.models.deletion +import oioioi.base.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0018_contest_show_contest_rules'), + ('programs', '0019_add_limits_override'), + ] + + operations = [ + migrations.AddField( + model_name='programsubmission', + name='user_language_code', + field=models.CharField(blank=True, max_length=6, null=True, verbose_name='User language code'), + ), + migrations.CreateModel( + name='CheckerFormatForProblem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('format', oioioi.base.fields.EnumField(max_length=64, verbose_name='format')), + ('problem_instance', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='contests.probleminstance', verbose_name='problem instance')), + ], + options={ + 'verbose_name': 'checker format for problem', + 'verbose_name_plural': 'checker formats for problems', + 'ordering': ('problem_instance',), + 'unique_together': {('problem_instance', 'format')}, + }, + ), + migrations.CreateModel( + name='CheckerFormatForContest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('format', oioioi.base.fields.EnumField(help_text="Format of the checker output for this contest. Abbreviated describes the output difference, while Terse doesn't give any details.", max_length=64, verbose_name='format')), + ('contest', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='contests.contest', verbose_name='contest')), + ], + options={ + 'verbose_name': 'checker format for contest', + 'verbose_name_plural': 'checker formats for contests', + 'ordering': ('contest',), + 'unique_together': {('contest', 'format')}, + }, + ), + ] diff --git a/oioioi/programs/models.py b/oioioi/programs/models.py index cd196e0b2..8f8b0920a 100644 --- a/oioioi/programs/models.py +++ b/oioioi/programs/models.py @@ -463,3 +463,44 @@ def check_compilers_config(): check_compilers_config() + + +CheckerFormat = EnumRegistry() +CheckerFormat.register('terse', _("Terse")) +CheckerFormat.register('abbreviated', _("Abbreviated")) + + +class CheckerFormatForContest(models.Model): + """Overrides the default checker's format (abbreviated) for a contest.""" + + contest = models.OneToOneField( + Contest, verbose_name=_("contest"), on_delete=models.CASCADE + ) + format = EnumField( + CheckerFormat, + verbose_name=_("format"), + help_text=_("Format of the checker output for this contest. " + "Abbreviated describes the output difference, while " + "Terse doesn't give any details.") + ) + + class Meta(object): + verbose_name = _("checker format for contest") + verbose_name_plural = _("checker formats for contests") + ordering = ('contest',) + unique_together = ('contest', 'format') + + +class CheckerFormatForProblem(models.Model): + """Overrides the default checker's format (abbreviated) for a problem.""" + + problem_instance = models.OneToOneField( + ProblemInstance, verbose_name=_("problem instance"), on_delete=models.CASCADE + ) + format = EnumField(CheckerFormat, verbose_name=_("format")) + + class Meta(object): + verbose_name = _("checker format for problem") + verbose_name_plural = _("checker formats for problems") + ordering = ('problem_instance',) + unique_together = ('problem_instance', 'format') diff --git a/oioioi/programs/tests.py b/oioioi/programs/tests.py index 6061df187..fa3c56d23 100644 --- a/oioioi/programs/tests.py +++ b/oioioi/programs/tests.py @@ -46,6 +46,8 @@ LanguageOverrideForTest, TestReport, check_compilers_config, + CheckerFormatForContest, + CheckerFormatForProblem, ) from oioioi.programs.problem_instance_utils import get_allowed_languages_dict from oioioi.programs.utils import form_field_id_for_langs @@ -544,7 +546,7 @@ def fake_send_notification( NotificationHandler.send_notification = fake_send_notification submission = Submission.objects.get(pk=1) - + environ = create_environ() environ['extra_args'] = {} environ['is_rejudge'] = False @@ -1986,3 +1988,65 @@ def test_proper_env_override(self): self.assertEqual( env_with_tests['tests']['1a']['exec_mem_limit'], tests[1].memory_limit ) + + +class TestOICompare(TestCase): + fixtures = ['test_users', 'test_contest'] + + def test_format(self): + contest = Contest.objects.get() + filename = get_test_filename('test_simple_package.zip') + self.assertTrue(self.client.login(username='test_admin')) + url = reverse('oioioiadmin:problems_problem_add') + response = self.client.get(url, {'contest_id': contest.id}, follow=True) + url = response.redirect_chain[-1][0] + self.assertEqual(response.status_code, 200) + self.assertIn( + 'problems/add-or-update.html', + [getattr(t, 'name', None) for t in response.templates], + ) + response = self.client.post( + url, + { + 'package_file': open(filename, 'rb'), + 'visibility': Problem.VISIBILITY_PRIVATE, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(Problem.objects.count(), 1) + + pi = ProblemInstance.objects.get(contest=contest) + for lang, terse, abbreviated in [ + ("pl", "ŹLE", 'ŹLE: wiersz 1: oczekiwano "50000000", otrzymano koniec pliku'), + ("en", "WRONG", 'WRONG: line 1: expected "50000000", got end of file'), + ]: + terse = terse.replace('"', '"') + abbreviated = abbreviated.replace('"', '"') + for format_contest, format_problem, expected, unexpected in [ + (None, None, abbreviated, None), + ('terse', None, terse, abbreviated), + ('terse', 'abbreviated', abbreviated, None), + (None, 'terse', terse, abbreviated), + ]: + CheckerFormatForContest.objects.all().delete() + CheckerFormatForProblem.objects.all().delete() + if format_problem is not None: + CheckerFormatForProblem.objects.create( + problem_instance=pi, format=format_problem + ) + if format_contest is not None: + CheckerFormatForContest.objects.create(contest=contest, format=format_contest) + + ps = ProgramSubmission.objects.create( + source_file=ContentFile(b'int main(){}', name='a.cpp'), + user_language_code=lang, + problem_instance=pi, + user=User.objects.get(username='test_admin'), + ) + pi.controller.judge(ps) + url = reverse('submission', kwargs={'contest_id': contest.id, 'submission_id': ps.id}) + response = self.client.get(url) + self.assertContains(response, expected) + if unexpected: + self.assertNotContains(response, unexpected) diff --git a/oioioi/programs/utils.py b/oioioi/programs/utils.py index 48c8ca210..94a4c64a4 100644 --- a/oioioi/programs/utils.py +++ b/oioioi/programs/utils.py @@ -17,6 +17,8 @@ ModelProgramSubmission, ProgramSubmission, ReportActionsConfig, + CheckerFormatForContest, + CheckerFormatForProblem, ) @@ -215,3 +217,16 @@ def get_submittable_languages(): for _, lang_config in submittable_languages.items(): lang_config.setdefault('type', 'main') return submittable_languages + + +def get_checker_format(problem_instance): + try: + return CheckerFormatForProblem.objects.get(problem_instance=problem_instance).format + except CheckerFormatForProblem.DoesNotExist: + if problem_instance.contest: + try: + return CheckerFormatForContest.objects.get(contest=problem_instance.contest).format + except CheckerFormatForContest.DoesNotExist: + return getattr(settings, 'DEFAULT_CHECKER_FORMAT', 'abbreviated') + else: + return getattr(settings, 'DEFAULT_CHECKER_FORMAT', 'abbreviated')