From b505700c396354589a0ae45cd3b5413825a3fa38 Mon Sep 17 00:00:00 2001 From: reivvax Date: Sun, 23 Mar 2025 14:38:16 +0100 Subject: [PATCH 01/12] Add limits visibility model and include it in admin panel --- .../migrations/0020_limitsvisibilityconfig.py | 27 +++++++++++++++ oioioi/contests/models.py | 21 ++++++++++++ oioioi/problems/admin.py | 33 +++++++++++++++++-- oioioi/problems/forms.py | 9 ++++- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 oioioi/contests/migrations/0020_limitsvisibilityconfig.py diff --git a/oioioi/contests/migrations/0020_limitsvisibilityconfig.py b/oioioi/contests/migrations/0020_limitsvisibilityconfig.py new file mode 100644 index 000000000..6a873b774 --- /dev/null +++ b/oioioi/contests/migrations/0020_limitsvisibilityconfig.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.20 on 2025-03-23 13:34 + +from django.db import migrations, models +import django.db.models.deletion +import oioioi.base.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0019_submissionmessage'), + ] + + operations = [ + migrations.CreateModel( + name='LimitsVisibilityConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('visible', oioioi.base.fields.EnumField(default='NO', help_text="Determines whether participants can see problems' time and memory limits", max_length=64, verbose_name='limits visibility')), + ('contest', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), + ], + options={ + 'verbose_name': 'limits visibility config', + 'verbose_name_plural': 'limits visibility configs', + }, + ), + ] diff --git a/oioioi/contests/models.py b/oioioi/contests/models.py index 7f7a85f17..6366b93ff 100644 --- a/oioioi/contests/models.py +++ b/oioioi/contests/models.py @@ -353,6 +353,27 @@ class Meta(object): verbose_name_plural = _("ranking visibility configs") +limits_visibility_options = EnumRegistry() +limits_visibility_options.register('YES', _("Visible")) +limits_visibility_options.register('NO', _("Not visible")) + + +class LimitsVisibilityConfig(models.Model): + contest = models.OneToOneField('contests.Contest', on_delete=models.CASCADE) + visible = EnumField( + limits_visibility_options, + default='NO', + verbose_name=_("limits visibility"), # TODO add the translation + help_text=_( + "Determines whether participants can see problems' time and memory limits" + ), + ) + + class Meta(object): + verbose_name = _("limits visibility config") # TODO add the translation + verbose_name_plural = _("limits visibility configs") # TODO add the translation + + registration_availability_options = EnumRegistry() registration_availability_options.register('YES', _("Open")) registration_availability_options.register('NO', _("Closed")) diff --git a/oioioi/problems/admin.py b/oioioi/problems/admin.py index 3023ef1f7..950253cec 100644 --- a/oioioi/problems/admin.py +++ b/oioioi/problems/admin.py @@ -21,7 +21,7 @@ from oioioi.contests.models import ( ProblemInstance, ProblemStatementConfig, - RankingVisibilityConfig, RegistrationAvailabilityConfig, + RankingVisibilityConfig, RegistrationAvailabilityConfig, LimitsVisibilityConfig, ) from oioioi.contests.utils import is_contest_admin, is_contest_basicadmin from oioioi.problems.forms import ( @@ -34,7 +34,7 @@ ProblemNameInlineFormSet, ProblemSiteForm, ProblemStatementConfigForm, - RankingVisibilityConfigForm, RegistrationAvailabilityConfigForm, + RankingVisibilityConfigForm, RegistrationAvailabilityConfigForm, LimitsVisibilityConfigForm, ) from oioioi.problems.models import ( AlgorithmTag, @@ -118,6 +118,35 @@ def __init__(self, *args, **kwargs): ContestAdmin.mix_in(RankingVisibilityConfigAdminMixin) +class LimitsVisibilityConfigInline(admin.TabularInline): + model = LimitsVisibilityConfig + extra = 1 + form = LimitsVisibilityConfigForm + category = _("Advanced") + + def has_add_permission(self, request, obj=None): + return is_contest_admin(request) + + def has_change_permission(self, request, obj=None): + return is_contest_admin(request) + + def has_delete_permission(self, request, obj=None): + return is_contest_admin(request) + + +class LimitsVisibilityConfigAdminMixin(object): + """Adds :class:`~oioioi.contests.models.LimitsVisibilityConfig` to an admin + panel. + """ + + def __init__(self, *args, **kwargs): + super(LimitsVisibilityConfigAdminMixin, self).__init__(*args, **kwargs) + self.inlines = tuple(self.inlines) + (LimitsVisibilityConfigInline,) + + +ContestAdmin.mix_in(LimitsVisibilityConfigAdminMixin) + + class RegistrationAvailabilityConfigInline(admin.TabularInline): model = RegistrationAvailabilityConfig extra = 1 diff --git a/oioioi/problems/forms.py b/oioioi/problems/forms.py index 4d93d98b5..c251ab6f5 100644 --- a/oioioi/problems/forms.py +++ b/oioioi/problems/forms.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from oioioi.base.utils.input_with_generate import TextInputWithGenerate from oioioi.base.utils.inputs import narrow_input_field -from oioioi.contests.models import ProblemStatementConfig, RankingVisibilityConfig, RegistrationAvailabilityConfig +from oioioi.contests.models import ProblemStatementConfig, RankingVisibilityConfig, LimitsVisibilityConfig, RegistrationAvailabilityConfig from oioioi.problems.models import OriginInfoValue, Problem, ProblemSite @@ -89,6 +89,13 @@ class Meta(object): widgets = {'visible': forms.RadioSelect()} +class LimitsVisibilityConfigForm(forms.ModelForm): + class Meta(object): + fields = '__all__' + model = LimitsVisibilityConfig + widgets = {'visible': forms.RadioSelect()} + + class RegistrationAvailabilityConfigForm(forms.ModelForm): class Meta(object): fields = '__all__' From 3223d9e47e4d87f8d4e3a8f5eec2d8a2a0a61b55 Mon Sep 17 00:00:00 2001 From: reivvax Date: Tue, 25 Mar 2025 00:27:18 +0100 Subject: [PATCH 02/12] Add fetching the tests data in controller --- oioioi/contests/controllers.py | 34 ++++++++++++++++++++++++++++++++++ oioioi/contests/utils.py | 3 +++ oioioi/contests/views.py | 10 +++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index 48c1bc6b3..dfa78a85f 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -12,6 +12,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_noop +from django.db.models import Min, Max from oioioi.base.utils import ( ObjectWithMixins, @@ -21,8 +22,10 @@ from oioioi.base.utils.query_helpers import Q_always_true from oioioi.contests.models import ( Contest, + ProblemInstance, ProblemStatementConfig, RankingVisibilityConfig, + LimitsVisibilityConfig, Round, RoundTimeExtension, ScoreReport, @@ -42,7 +45,9 @@ rounds_times, visible_problem_instances, ) +from oioioi.newsfeed import default_app_config from oioioi.problems.controllers import ProblemController +from oioioi.programs.models import Test logger = logging.getLogger(__name__) @@ -586,6 +591,19 @@ def can_see_statement(self, request_or_context, problem_instance): def default_can_see_statement(self, request_or_context, problem_instance): return True + def can_see_problems_limits(self, request): + context = self.make_context(request) + lvc = LimitsVisibilityConfig.objects.filter(contest=context.contest) + if lvc.exists() and lvc[0].visible == 'YES': + return True + elif lvc.exits() and lvc[0].visible == 'NO': + return False + else: + return self.default_can_see_problems_limits(request) + + def default_can_see_problems_limits(self, request): + return False + def can_submit(self, request, problem_instance, check_round_times=True): """Determines if the current user is allowed to submit a solution for the given problem. @@ -641,6 +659,22 @@ def is_submissions_limit_exceeded(self, request, problem_instance, kind=None): request, problem_instance, kind ) + def get_problems_limits(self, request): + """Returns a dictionary containing data about time and memory limits for a given ProblemInstance: + ProblemInstance_ID -> (min_time_limit, max_time_limit, min_memory_limit, max_memory_limit) + """ + instances = ProblemInstance.objects.filter(contest=request.contest).annotate( + min_time=Min("test_set__time_limit"), + max_time=Max("test_set__time_limit"), + min_memory=Min("test_set__memory_limit"), + max_memory=Max("test_set__memory_limit") + ).values("id", "min_time", "max_time", "min_memory", "max_memory") + + instances_to_limits = {instance['id']: (instance['min_time'], instance['max_time'], instance['min_memory'], instance['max_memory']) + for instance in instances} + + return instances_to_limits + def adjust_submission_form(self, request, form, problem_instance): # by default delegate to ProblemController problem_instance.problem.controller.adjust_submission_form( diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 05afdffd9..2a4f2a718 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -754,3 +754,6 @@ def get_problem_statements(request, controller, problem_instances): ], key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), ) + +def stringify_problems_limits(raw_limits): + pass \ No newline at end of file diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index 69f5ba0bb..d005f7144 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -64,6 +64,7 @@ are_rules_visible, get_scoring_desription, get_submission_message, + stringify_problems_limits, ) from oioioi.filetracker.utils import stream_file from oioioi.problems.models import ProblemAttachment, ProblemStatement @@ -185,20 +186,27 @@ def problems_list_view(request): key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), ) + show_problems_limits = controller.can_see_problems_limits(request) show_submissions_limit = any([p[5] for p in problems_statements]) show_submit_button = any([p[6] for p in problems_statements]) show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 - table_columns = 3 + int(show_submissions_limit) + int(show_submit_button) + table_columns = 3 + int(show_problems_limits) + int(show_submissions_limit) + int(show_submit_button) + + problems_limits = None + if show_problems_limits: + problems_limits = stringify_problems_limits(controller.get_problems_limits(request)) return TemplateResponse( request, 'contests/problems_list.html', { 'problem_instances': problems_statements, + 'show_problems_limits': show_problems_limits, 'show_rounds': show_rounds, 'show_scores': request.user.is_authenticated, 'show_submissions_limit': show_submissions_limit, 'show_submit_button': show_submit_button, + 'problems_limits': problems_limits, 'table_columns': table_columns, 'problems_on_page': getattr(settings, 'PROBLEMS_ON_PAGE', 100), }, From 078b9dce1511d8c05be1144a3bb763e3ee84954d Mon Sep 17 00:00:00 2001 From: reivvax Date: Tue, 25 Mar 2025 14:24:04 +0100 Subject: [PATCH 03/12] Update the query to include language overrides --- oioioi/contests/controllers.py | 58 ++++++++++++++++++++++++++++------ oioioi/contests/views.py | 2 +- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index dfa78a85f..e2c04be47 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -12,7 +12,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_noop -from django.db.models import Min, Max +from django.db.models import Min, Max, Q from oioioi.base.utils import ( ObjectWithMixins, @@ -661,17 +661,55 @@ def is_submissions_limit_exceeded(self, request, problem_instance, kind=None): def get_problems_limits(self, request): """Returns a dictionary containing data about time and memory limits for a given ProblemInstance: - ProblemInstance_ID -> (min_time_limit, max_time_limit, min_memory_limit, max_memory_limit) + ProblemInstanceID -> { + 'default': (min_time, max_time, min_memory, max_memory), + 'cpp': (min_time, max_time, min_memory, max_memory), + 'py': (min_time, max_time, min_memory, max_memory) + } + Corresponding tuples are None if no limits exist """ instances = ProblemInstance.objects.filter(contest=request.contest).annotate( - min_time=Min("test_set__time_limit"), - max_time=Max("test_set__time_limit"), - min_memory=Min("test_set__memory_limit"), - max_memory=Max("test_set__memory_limit") - ).values("id", "min_time", "max_time", "min_memory", "max_memory") - - instances_to_limits = {instance['id']: (instance['min_time'], instance['max_time'], instance['min_memory'], instance['max_memory']) - for instance in instances} + min_time=Min('test_set__time_limit', filter=Q(test_set__is_active=True)), + max_time=Max('test_set__time_limit', filter=Q(test_set__is_active=True)), + min_memory=Min('test_set__memory_limit', filter=Q(test_set__is_active=True)), + max_memory=Max('test_set__memory_limit', filter=Q(test_set__is_active=True)), + cpp_min_time=Min('test_set__languageoverridefortest_set__time_limit', + filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), + cpp_max_time=Max('test_set__languageoverridefortest_set__time_limit', + filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), + cpp_min_memory=Min('test_set__languageoverridefortest_set__memory_limit', + filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), + cpp_max_memory=Max('test_set__languageoverridefortest_set__memory_limit', + filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), + py_min_time=Min('test_set__languageoverridefortest_set__time_limit', + filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), + py_max_time=Max('test_set__languageoverridefortest_set__time_limit', + filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), + py_min_memory=Min('test_set__languageoverridefortest_set__memory_limit', + filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), + py_max_memory=Max('test_set__languageoverridefortest_set__memory_limit', + filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), + ).values( + 'id', + 'min_time', 'max_time', 'min_memory', 'max_memory', + 'cpp_min_time', 'cpp_max_time', 'cpp_min_memory', 'cpp_max_memory', + 'py_min_time', 'py_max_time', 'py_min_memory', 'py_max_memory' + ) + + instances_to_limits = { + instance["id"]: { + 'default': + (instance["min_time"], instance["max_time"], instance["min_memory"], instance["max_memory"]) + if instance['cpp_min_time'] is not None else None, + 'cpp': + (instance["cpp_min_time"], instance["cpp_max_time"], instance["cpp_min_memory"], instance["cpp_max_memory"]) + if instance['cpp_min_time'] is not None else None, + 'py': + (instance["py_min_time"], instance["py_max_time"], instance["py_min_memory"], instance["py_max_memory"]) + if instance['py_min_time'] is not None else None + } + for instance in instances + } return instances_to_limits diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index d005f7144..fa39cb4ca 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -192,7 +192,7 @@ def problems_list_view(request): show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 table_columns = 3 + int(show_problems_limits) + int(show_submissions_limit) + int(show_submit_button) - problems_limits = None + problems_limits = {} if show_problems_limits: problems_limits = stringify_problems_limits(controller.get_problems_limits(request)) From bcafc9824d0e2ec46e1bc0712b2f15d79cf3bd76 Mon Sep 17 00:00:00 2001 From: reivvax Date: Mon, 31 Mar 2025 00:25:20 +0200 Subject: [PATCH 04/12] Adjust the query, add fixutures and test --- oioioi/contests/controllers.py | 142 ++++++++++++---- oioioi/contests/tests/tests.py | 53 ++++++ oioioi/contests/utils.py | 4 + ...t_program_tests_and_languageoverrides.json | 159 ++++++++++++++++++ 4 files changed, 323 insertions(+), 35 deletions(-) create mode 100644 oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index e2c04be47..e99cfc1f4 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -668,48 +668,120 @@ def get_problems_limits(self, request): } Corresponding tuples are None if no limits exist """ + + # split it to two queries instances = ProblemInstance.objects.filter(contest=request.contest).annotate( - min_time=Min('test_set__time_limit', filter=Q(test_set__is_active=True)), - max_time=Max('test_set__time_limit', filter=Q(test_set__is_active=True)), - min_memory=Min('test_set__memory_limit', filter=Q(test_set__is_active=True)), - max_memory=Max('test_set__memory_limit', filter=Q(test_set__is_active=True)), - cpp_min_time=Min('test_set__languageoverridefortest_set__time_limit', - filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), - cpp_max_time=Max('test_set__languageoverridefortest_set__time_limit', - filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), - cpp_min_memory=Min('test_set__languageoverridefortest_set__memory_limit', - filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), - cpp_max_memory=Max('test_set__languageoverridefortest_set__memory_limit', - filter=Q(test_set__languageoverridefortest_set__language='cpp') & Q(test_set__is_active=True)), - py_min_time=Min('test_set__languageoverridefortest_set__time_limit', - filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), - py_max_time=Max('test_set__languageoverridefortest_set__time_limit', - filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), - py_min_memory=Min('test_set__languageoverridefortest_set__memory_limit', - filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), - py_max_memory=Max('test_set__languageoverridefortest_set__memory_limit', - filter=Q(test_set__languageoverridefortest_set__language='py') & Q(test_set__is_active=True)), + # default limits + min_time=Min('test__time_limit', filter=Q(test__is_active=True)), + max_time=Max('test__time_limit', filter=Q(test__is_active=True)), + min_memory=Min('test__memory_limit', filter=Q(test__is_active=True)), + max_memory=Max('test__memory_limit', filter=Q(test__is_active=True)), + + # cpp overridden limits + cpp_min_time=Min( + 'test__languageoverridefortest__time_limit', + filter=Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + cpp_max_time=Max( + 'test__languageoverridefortest__time_limit', + filter=Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + cpp_min_memory=Min( + 'test__languageoverridefortest__memory_limit', + filter=Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + cpp_max_memory=Max( + 'test__languageoverridefortest__memory_limit', + filter=Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + + # python overridden limits + py_min_time=Min( + 'test__languageoverridefortest__time_limit', + filter=Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + py_max_time=Max( + 'test__languageoverridefortest__time_limit', + filter=Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + py_min_memory=Min( + 'test__languageoverridefortest__memory_limit', + filter=Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + py_max_memory=Max( + 'test__languageoverridefortest__memory_limit', + filter=Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + ).annotate( + # non-overridden test limits in cpp + cpp_min_time_non_overridden=Min( + 'test__time_limit', + filter=~Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + cpp_max_time_non_overridden=Max( + 'test__time_limit', + filter=~Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + cpp_min_memory_non_overridden=Min( + 'test__memory_limit', + filter=~Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + cpp_max_memory_non_overridden=Max( + 'test__memory_limit', + filter=~Q(test__languageoverridefortest__language='cpp') & Q(test__is_active=True) + ), + + # non-overridden test limits in python + py_min_time_non_overridden=Min( + 'test__time_limit', + filter=~Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + py_max_time_non_overridden=Max( + 'test__time_limit', + filter=~Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + py_min_memory_non_overridden=Min( + 'test__memory_limit', + filter=~Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), + py_max_memory_non_overridden=Max( + 'test__memory_limit', + filter=~Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) + ), ).values( 'id', 'min_time', 'max_time', 'min_memory', 'max_memory', 'cpp_min_time', 'cpp_max_time', 'cpp_min_memory', 'cpp_max_memory', - 'py_min_time', 'py_max_time', 'py_min_memory', 'py_max_memory' + 'cpp_min_time_non_overridden', 'cpp_max_time_non_overridden', 'cpp_min_memory_non_overridden', 'cpp_max_memory_non_overridden', + 'py_min_time', 'py_max_time', 'py_min_memory', 'py_max_memory', + 'py_min_time_non_overridden', 'py_max_time_non_overridden', 'py_min_memory_non_overridden', 'py_max_memory_non_overridden' ) - instances_to_limits = { - instance["id"]: { - 'default': - (instance["min_time"], instance["max_time"], instance["min_memory"], instance["max_memory"]) - if instance['cpp_min_time'] is not None else None, - 'cpp': - (instance["cpp_min_time"], instance["cpp_max_time"], instance["cpp_min_memory"], instance["cpp_max_memory"]) - if instance['cpp_min_time'] is not None else None, - 'py': - (instance["py_min_time"], instance["py_max_time"], instance["py_min_memory"], instance["py_max_memory"]) - if instance['py_min_time'] is not None else None - } - for instance in instances - } + instances_to_limits = {} + + for instance in instances: + if instance['min_time'] is not None: + instances_to_limits[instance['id']] = { + 'default': (instance['min_time'], instance['max_time'], instance['min_memory'], instance['max_memory']), + 'cpp': ( + min(filter(None, [instance['cpp_min_time'], instance['cpp_min_time_non_overridden']])), + max(filter(None, [instance['cpp_max_time'], instance['cpp_max_time_non_overridden']])), + min(filter(None, [instance['cpp_min_memory'], instance['cpp_min_memory_non_overridden']])), + max(filter(None, [instance['cpp_max_memory'], instance['cpp_max_memory_non_overridden']])) + ), + 'py': ( + min(filter(None, [instance['py_min_time'], instance['py_min_time_non_overridden']])), + max(filter(None, [instance['py_max_time'], instance['py_max_time_non_overridden']])), + min(filter(None, [instance['py_min_memory'], instance['py_min_memory_non_overridden']])), + max(filter(None, [instance['py_max_memory'], instance['py_max_memory_non_overridden']])) + ), + } + else: + instances_to_limits[instance['id']] = { + 'default': None, + 'cpp': None, + 'py': None + } return instances_to_limits diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index a6c4fd587..57b55f1d4 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -568,6 +568,59 @@ def __init__(self, timestamp, contest): ) +class TestProblemLimitsFetching(TestCase): + fixtures = [ + 'test_contest', + 'test_program_tests_and_languageoverrides' + ] + + def test_problems_limits_for_contest_view(self): + contest = Contest.objects.get() + + class FakeRequest(object): + def __init__(self, timestamp, contest): + self.timestamp = timestamp + self.user = AnonymousUser() + self.contest = contest + + limits = contest.controller.get_problems_limits( + FakeRequest(datetime(2011, 1, 1, tzinfo=timezone.utc), contest) + ) + + # Test defaults + self.assertEqual(limits[1]['default'][0], 1000) + self.assertEqual(limits[1]['default'][1], 2000) + self.assertEqual(limits[1]['default'][2], 256000) + self.assertEqual(limits[1]['default'][3], 512000) + + self.assertEqual(limits[2]['default'][0], 1000) + self.assertEqual(limits[2]['default'][1], 4000) + self.assertEqual(limits[2]['default'][2], 128000) + self.assertEqual(limits[2]['default'][3], 1024000) + + # Test cpp + self.assertEqual(limits[1]['cpp'][0], 1000) + self.assertEqual(limits[1]['cpp'][1], 2000) + self.assertEqual(limits[1]['cpp'][2], 256000) + self.assertEqual(limits[1]['cpp'][3], 512000) + + self.assertEqual(limits[2]['cpp'][0], 500) + self.assertEqual(limits[2]['cpp'][1], 4000) + self.assertEqual(limits[2]['cpp'][2], 64000) + self.assertEqual(limits[2]['cpp'][3], 1024000) + + # Test python + self.assertEqual(limits[1]['py'][0], 1000) + self.assertEqual(limits[1]['py'][1], 2000) + self.assertEqual(limits[1]['py'][2], 256000) + self.assertEqual(limits[1]['py'][3], 512000) + + self.assertEqual(limits[2]['py'][0], 1000) + self.assertEqual(limits[2]['py'][1], 6000) + self.assertEqual(limits[2]['py'][2], 128000) + self.assertEqual(limits[2]['py'][3], 2048000) + + class TestContestViews(TestCase): fixtures = [ 'test_users', diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 2a4f2a718..3ff8bd0f9 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -756,4 +756,8 @@ def get_problem_statements(request, controller, problem_instances): ) def stringify_problems_limits(raw_limits): + """Returns human readable ready to render version of limits for given problem instances. + """ + for pi_pk, pi_limits in raw_limits.values(): + pass pass \ No newline at end of file diff --git a/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json b/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json new file mode 100644 index 000000000..d0dddaa29 --- /dev/null +++ b/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json @@ -0,0 +1,159 @@ +[ + { + "pk": 1, + "model": "problems.problem", + "fields": { + "legacy_name": "24_s2 1", + "short_name": "24_s2 1", + "visibility": "PU" + } + }, + { + "pk": 2, + "model": "problems.problem", + "fields": { + "legacy_name": "24_s2 2", + "short_name": "24_s2 2", + "visibility": "PU" + } + }, + { + "pk": 1, + "model": "contests.probleminstance", + "fields": { + "problem": 1, + "round": 1, + "short_name": "zad1", + "contest": "c" + } + }, + { + "pk": 2, + "model": "contests.probleminstance", + "fields": { + "problem": 2, + "round": 1, + "short_name": "zad2", + "contest": "c" + } + }, + + + { + "model": "programs.test", + "pk": 1, + "fields": { + "problem_instance": 1, + "name": "test1", + "kind": "NORMAL", + "group": "g1", + "time_limit": 1000, + "memory_limit": 256000 + } + }, + { + "model": "programs.test", + "pk": 2, + "fields": { + "problem_instance": 1, + "name": "test2", + "kind": "NORMAL", + "group": "g2", + "time_limit": 2000, + "memory_limit": 512000 + } + }, + + + { + "model": "programs.test", + "pk": 3, + "fields": { + "problem_instance": 2, + "name": "test3", + "kind": "NORMAL", + "group": "g1", + "time_limit": 1000, + "memory_limit": 128000 + } + }, + { + "model": "programs.test", + "pk": 4, + "fields": { + "problem_instance": 2, + "name": "test4", + "kind": "NORMAL", + "group": "g2", + "time_limit": 2000, + "memory_limit": 256000 + } + }, + { + "model": "programs.test", + "pk": 5, + "fields": { + "problem_instance": 2, + "name": "test5", + "kind": "NORMAL", + "group": "g3", + "time_limit": 3000, + "memory_limit": 512000 + } + }, + { + "model": "programs.test", + "pk": 6, + "fields": { + "problem_instance": 2, + "name": "test6", + "kind": "NORMAL", + "group": "g4", + "time_limit": 4000, + "memory_limit": 1024000 + } + }, + + + { + "model": "programs.languageoverridefortest", + "pk": 1, + "fields": { + "test": 3, + "time_limit": 500, + "memory_limit": 64000, + "language": "cpp" + } + }, + { + "model": "programs.languageoverridefortest", + "pk": 2, + "fields": { + "test": 4, + "time_limit": 1000, + "memory_limit": 128000, + "language": "cpp" + } + }, + + { + "model": "programs.languageoverridefortest", + "pk": 3, + "fields": { + "test": 5, + "time_limit": 5000, + "memory_limit": 1024000, + "language": "py" + } + }, + { + "model": "programs.languageoverridefortest", + "pk": 4, + "fields": { + "test": 6, + "time_limit": 6000, + "memory_limit": 2048000, + "language": "py" + } + } +] \ No newline at end of file From f336c64712f1a7786293aedaa87b153192a18c45 Mon Sep 17 00:00:00 2001 From: reivvax Date: Tue, 1 Apr 2025 19:13:50 +0200 Subject: [PATCH 05/12] Add stringification of limits --- oioioi/contests/controllers.py | 10 +---- oioioi/contests/tests/tests.py | 46 +++++++++++++++++++++++ oioioi/contests/utils.py | 69 ++++++++++++++++++++++++++++++++-- 3 files changed, 113 insertions(+), 12 deletions(-) diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index e99cfc1f4..8bc29e284 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -666,7 +666,7 @@ def get_problems_limits(self, request): 'cpp': (min_time, max_time, min_memory, max_memory), 'py': (min_time, max_time, min_memory, max_memory) } - Corresponding tuples are None if no limits exist + Corresponding dictionary is None if no limits exist """ # split it to two queries @@ -712,7 +712,7 @@ def get_problems_limits(self, request): 'test__languageoverridefortest__memory_limit', filter=Q(test__languageoverridefortest__language='py') & Q(test__is_active=True) ), - ).annotate( + # non-overridden test limits in cpp cpp_min_time_non_overridden=Min( 'test__time_limit', @@ -776,12 +776,6 @@ def get_problems_limits(self, request): max(filter(None, [instance['py_max_memory'], instance['py_max_memory_non_overridden']])) ), } - else: - instances_to_limits[instance['id']] = { - 'default': None, - 'cpp': None, - 'py': None - } return instances_to_limits diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 57b55f1d4..6e64d4c5c 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -59,6 +59,7 @@ is_contest_basicadmin, is_contest_observer, rounds_times, + stringify_problems_limits, ) from oioioi.dashboard.contest_dashboard import unregister_contest_dashboard_view from oioioi.filetracker.tests import TestStreamingMixin @@ -4552,3 +4553,48 @@ def test_score_badge(self): self.assertIn('badge-success', self._get_badge_for_problem(response.content, 'zad1')) self.assertIn('badge-warning', self._get_badge_for_problem(response.content, 'zad2')) self.assertIn('badge-danger', self._get_badge_for_problem(response.content, 'zad3')) + + +class TestUtils(TestCase): + + def test_stringify_limits(self): + raw_limits = { + '1': { + 'default': (1000, 2000, 1000, 2000), + 'cpp': (1000, 2000, 1000, 2000), + 'py': (1000, 2000, 1000, 2000), + }, + '2': { + 'default': (1000, 2000, 1000, 2000), + 'cpp': (500, 2000, 500, 1000), + 'py': (1000, 3000, 1000, 3000), + }, + '3': { + 'default': (1000, 2000, 1000, 2000), + 'cpp': (500, 2000, 500, 1000), + 'py': (1000, 2000, 1000, 2000), + } + } + + stringified = stringify_problems_limits(raw_limits) + print(stringified) + self.assertEqual(len(stringified['1']), 1) + self.assertEqual(stringified['1'][0][0], '') + self.assertEqual(stringified['1'][0][1], '1-2 s') + # self.assertEqual(stringified['1'][0][2], '1-2 MB') + + self.assertEqual(len(stringified['2']), 2) + self.assertEqual(stringified['2'][0][0], 'C++:') + self.assertEqual(stringified['2'][0][1], '0.5-2 s') + # self.assertEqual(stringified['2'][0][2], '0.51-1 MB') + self.assertEqual(stringified['2'][1][0], 'Python:') + self.assertEqual(stringified['2'][1][1], '1-3 s') + # self.assertEqual(stringified['2'][1][2], '1-3.1 MB') + + self.assertEqual(len(stringified['2']), 2) + self.assertEqual(stringified['2'][0][0], 'Default:') + self.assertEqual(stringified['2'][0][1], '1-2 s') + # self.assertEqual(stringified['2'][0][2], '1-2 MB') + self.assertEqual(stringified['2'][1][0], 'C++:') + self.assertEqual(stringified['2'][1][1], '0.5-2 s') + # self.assertEqual(stringified['2'][1][2], '0.51-1 MB') diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 3ff8bd0f9..4b3a7d786 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -756,8 +756,69 @@ def get_problem_statements(request, controller, problem_instances): ) def stringify_problems_limits(raw_limits): - """Returns human readable ready to render version of limits for given problem instances. + """Stringifies the time and memory limits for a given set of problem instances. + + This function processes a dictionary of problem instances (raw_limits), where each problem instance + contains limits for default, C++, and Python. The function then formats these limits into + human-readable strings based on the following logic: + - If both C++ and Python limits are the same as the default, only the default limits are shown. + - Else if both limits for C++ or Python differ from the default limits, those limits are formatted separately. + - Else if one of language's limits differ, the default and the differing language limits are shown. + + Args: + raw_limits (dict): A dictionary of problem instances, where each key is the problem instance ID and + each value is another dictionary containing the following keys: + - 'default': A tuple (min_time, max_time, min_memory, max_memory) for the default limits. + - 'cpp': A tuple (min_time, max_time, min_memory, max_memory) for C++ language. + - 'py': A tuple (min_time, max_time, min_memory, max_memory) for Python language. + + Returns: + dict: A dictionary of formatted limits, where each key is the problem instance ID and each value is + a tuple with the following format: + - For default-only limits: (('', time_limit, memory_limit),) + - For limits with both languages: (('C++:', cpp_time, cpp_memory), ('Python:', py_time, py_memory)) + - For mixed limits (one language differs): (('Default:', time_limit, memory_limit), language_limits) """ - for pi_pk, pi_limits in raw_limits.values(): - pass - pass \ No newline at end of file + def KiB_to_MB(KiBs): + return KiBs * 1024 / 1000000 + + def format_limits(pi_limits): + if pi_limits[0] == pi_limits[1]: # time does not vary + time_limit = f'{pi_limits[0] / 1000:.2g} s' + else: + time_limit = f'{pi_limits[0] / 1000:.2g}-{pi_limits[1] / 1000:.2g} s' + + if pi_limits[2] == pi_limits[3]: # memory does not vary + memory_limit = f'{KiB_to_MB(pi_limits[2]):.2g} MB' + else: + memory_limit = f'{KiB_to_MB(pi_limits[2]):.2g}-{KiB_to_MB(pi_limits[3]):.2g} MB' + + return time_limit, memory_limit + + stringified = {} + + for pi_pk, pi_limits in raw_limits.items(): + if all(pi_limits[lang] == pi_limits['default'] for lang in ['cpp', 'py']): # language limits same as default + time_limit, memory_limit = format_limits(pi_limits['default']) + stringified[pi_pk] = (('', time_limit, memory_limit),) + + elif all(pi_limits[lang] != pi_limits['default'] for lang in ['cpp', 'py']): # both languages differ + cpp_time, cpp_memory = format_limits(pi_limits['cpp']) + py_time, py_memory = format_limits(pi_limits['py']) + stringified[pi_pk] = ( + ('C++:', cpp_time, cpp_memory), + ('Python:', py_time, py_memory) + ) + + else: # one of languages differ + if pi_limits['cpp'] != pi_limits['default']: + language_limits = ('C++:', *format_limits(pi_limits['cpp'])) + else: + language_limits = ('Python:', *format_limits(pi_limits['py'])) + + stringified[pi_pk] = ( + ('Default:', *format_limits(pi_limits['default'])), + language_limits + ) + + return stringified \ No newline at end of file From 952a58fe5e7e782df33ac8e33c779f71e7a6d635 Mon Sep 17 00:00:00 2001 From: reivvax Date: Tue, 1 Apr 2025 19:50:47 +0200 Subject: [PATCH 06/12] Adjust the template --- .../templates/contests/problems_list.html | 25 +++++++++++++++++-- oioioi/contests/views.py | 13 +++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index 9c5e83932..cb170e3c4 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -15,6 +15,11 @@

{% trans "Problems" %}

{% trans "Name" %} + {% if show_problems_limits %} + + Limits + + {% endif %} {% if show_submissions_limit %} {% if user.is_authenticated %} @@ -31,8 +36,7 @@

{% trans "Problems" %}

- {% for pi, statement_visible, round_time, result, submissions_left, submissions_limit, can_submit in problem_instances %} - + {% for pi, statement_visible, round_time, problem_limits, result, submissions_left, submissions_limit, can_submit in problem_instances %} {% if show_rounds %} {% ifchanged pi.round %} @@ -53,6 +57,23 @@

{% trans "Problems" %}

{{ pi.problem.name }} {% endif %} + {% if show_problems_limits %} + + + {% for row in problem_limits %} + {% if row %} + + {% for entry in row %} + + {% endfor %} + + {% endif %} + {% endfor %} +
+ {{ entry }} +
+ + {% endif %} {% if show_submissions_limit %} {% if submissions_left == None %} diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index fa39cb4ca..220ed5515 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -142,6 +142,12 @@ def contest_rules_view(request): @enforce_condition(contest_exists & can_enter_contest) def problems_list_view(request): controller = request.contest.controller + + show_problems_limits = controller.can_see_problems_limits(request) + problems_limits = {} + if show_problems_limits: + problems_limits = stringify_problems_limits(controller.get_problems_limits(request)) + problem_instances = visible_problem_instances(request) # Problem statements in order @@ -159,6 +165,7 @@ def problems_list_view(request): pi, controller.can_see_statement(request, pi), controller.get_round_times(request, pi.round), + problems_limits[pi.pk], # Because this view can be accessed by an anynomous user we can't # use `user=request.user` (it would cause TypeError). Surprisingly # using request.user.id is ok since for AnynomousUser id is set @@ -186,16 +193,11 @@ def problems_list_view(request): key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), ) - show_problems_limits = controller.can_see_problems_limits(request) show_submissions_limit = any([p[5] for p in problems_statements]) show_submit_button = any([p[6] for p in problems_statements]) show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 table_columns = 3 + int(show_problems_limits) + int(show_submissions_limit) + int(show_submit_button) - problems_limits = {} - if show_problems_limits: - problems_limits = stringify_problems_limits(controller.get_problems_limits(request)) - return TemplateResponse( request, 'contests/problems_list.html', @@ -206,7 +208,6 @@ def problems_list_view(request): 'show_scores': request.user.is_authenticated, 'show_submissions_limit': show_submissions_limit, 'show_submit_button': show_submit_button, - 'problems_limits': problems_limits, 'table_columns': table_columns, 'problems_on_page': getattr(settings, 'PROBLEMS_ON_PAGE', 100), }, From 6a845f4df3d7a9415c0a8488b136fb857f8288a7 Mon Sep 17 00:00:00 2001 From: reivvax Date: Wed, 2 Apr 2025 17:23:58 +0200 Subject: [PATCH 07/12] Minor update --- oioioi/contests/controllers.py | 1 - oioioi/contests/models.py | 6 +++--- oioioi/contests/templates/contests/problems_list.html | 4 ++-- oioioi/contests/utils.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index 8bc29e284..b724530ba 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -669,7 +669,6 @@ def get_problems_limits(self, request): Corresponding dictionary is None if no limits exist """ - # split it to two queries instances = ProblemInstance.objects.filter(contest=request.contest).annotate( # default limits min_time=Min('test__time_limit', filter=Q(test__is_active=True)), diff --git a/oioioi/contests/models.py b/oioioi/contests/models.py index 6366b93ff..42cf705a5 100644 --- a/oioioi/contests/models.py +++ b/oioioi/contests/models.py @@ -363,15 +363,15 @@ class LimitsVisibilityConfig(models.Model): visible = EnumField( limits_visibility_options, default='NO', - verbose_name=_("limits visibility"), # TODO add the translation + verbose_name=_("limits visibility"), help_text=_( "Determines whether participants can see problems' time and memory limits" ), ) class Meta(object): - verbose_name = _("limits visibility config") # TODO add the translation - verbose_name_plural = _("limits visibility configs") # TODO add the translation + verbose_name = _("limits visibility config") + verbose_name_plural = _("limits visibility configs") registration_availability_options = EnumRegistry() diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index cb170e3c4..546a4da1b 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -16,8 +16,8 @@

{% trans "Problems" %}

{% trans "Name" %} {% if show_problems_limits %} - - Limits + + {% trans "Limits" %} {% endif %} {% if show_submissions_limit %} diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 4b3a7d786..8c9f14c64 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -817,7 +817,7 @@ def format_limits(pi_limits): language_limits = ('Python:', *format_limits(pi_limits['py'])) stringified[pi_pk] = ( - ('Default:', *format_limits(pi_limits['default'])), + (_('Default') + ':', *format_limits(pi_limits['default'])), language_limits ) From 6e90045f6df36b69cb93d3e53ad93cc1fb2d8451 Mon Sep 17 00:00:00 2001 From: reivvax Date: Wed, 2 Apr 2025 18:12:26 +0200 Subject: [PATCH 08/12] Improve limits computing and rendering --- oioioi/contests/controllers.py | 23 +--------- oioioi/contests/static/common/contests.scss | 10 +++++ .../templates/contests/problems_list.html | 2 +- oioioi/contests/utils.py | 45 +++++++++++++++---- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index b724530ba..dc086d1ab 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -44,6 +44,7 @@ last_break_between_rounds, rounds_times, visible_problem_instances, + process_instances_to_limits, ) from oioioi.newsfeed import default_app_config from oioioi.problems.controllers import ProblemController @@ -756,27 +757,7 @@ def get_problems_limits(self, request): 'py_min_time_non_overridden', 'py_max_time_non_overridden', 'py_min_memory_non_overridden', 'py_max_memory_non_overridden' ) - instances_to_limits = {} - - for instance in instances: - if instance['min_time'] is not None: - instances_to_limits[instance['id']] = { - 'default': (instance['min_time'], instance['max_time'], instance['min_memory'], instance['max_memory']), - 'cpp': ( - min(filter(None, [instance['cpp_min_time'], instance['cpp_min_time_non_overridden']])), - max(filter(None, [instance['cpp_max_time'], instance['cpp_max_time_non_overridden']])), - min(filter(None, [instance['cpp_min_memory'], instance['cpp_min_memory_non_overridden']])), - max(filter(None, [instance['cpp_max_memory'], instance['cpp_max_memory_non_overridden']])) - ), - 'py': ( - min(filter(None, [instance['py_min_time'], instance['py_min_time_non_overridden']])), - max(filter(None, [instance['py_max_time'], instance['py_max_time_non_overridden']])), - min(filter(None, [instance['py_min_memory'], instance['py_min_memory_non_overridden']])), - max(filter(None, [instance['py_max_memory'], instance['py_max_memory_non_overridden']])) - ), - } - - return instances_to_limits + return process_instances_to_limits(instances) def adjust_submission_form(self, request, form, problem_instance): # by default delegate to ProblemController diff --git a/oioioi/contests/static/common/contests.scss b/oioioi/contests/static/common/contests.scss index bb12a3274..956d787e9 100644 --- a/oioioi/contests/static/common/contests.scss +++ b/oioioi/contests/static/common/contests.scss @@ -45,3 +45,13 @@ table.table-striped > tbody > tr.problemlist-subheader { margin: 3rem; margin-bottom: 4rem; } + +.limits-row { + background-color: transparent !important; + border-color: transparent !important; + white-space: nowrap; +} + +.limits-row td { + border: none; +} diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index 546a4da1b..e56d41ab2 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -62,7 +62,7 @@

{% trans "Problems" %}

{% for row in problem_limits %} {% if row %} - + {% for entry in row %} - + {% if show_problems_limits %}
{{ entry }} diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 8c9f14c64..3c034d58c 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -755,6 +755,29 @@ def get_problem_statements(request, controller, problem_instances): key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), ) +def process_instances_to_limits(raw_instances): + instances_to_limits = {} + + for instance in raw_instances: + if instance['min_time'] is not None: + instances_to_limits[instance['id']] = { + 'default': (instance['min_time'], instance['max_time'], instance['min_memory'], instance['max_memory']), + 'cpp': ( + min(filter(None, [instance['cpp_min_time'], instance['cpp_min_time_non_overridden']])), + max(filter(None, [instance['cpp_max_time'], instance['cpp_max_time_non_overridden']])), + min(filter(None, [instance['cpp_min_memory'], instance['cpp_min_memory_non_overridden']])), + max(filter(None, [instance['cpp_max_memory'], instance['cpp_max_memory_non_overridden']])) + ), + 'py': ( + min(filter(None, [instance['py_min_time'], instance['py_min_time_non_overridden']])), + max(filter(None, [instance['py_max_time'], instance['py_max_time_non_overridden']])), + min(filter(None, [instance['py_min_memory'], instance['py_min_memory_non_overridden']])), + max(filter(None, [instance['py_max_memory'], instance['py_max_memory_non_overridden']])) + ), + } + + return instances_to_limits + def stringify_problems_limits(raw_limits): """Stringifies the time and memory limits for a given set of problem instances. @@ -780,18 +803,24 @@ def stringify_problems_limits(raw_limits): - For mixed limits (one language differs): (('Default:', time_limit, memory_limit), language_limits) """ def KiB_to_MB(KiBs): - return KiBs * 1024 / 1000000 + return KiBs * 1024 // 1000000 def format_limits(pi_limits): - if pi_limits[0] == pi_limits[1]: # time does not vary - time_limit = f'{pi_limits[0] / 1000:.2g} s' - else: - time_limit = f'{pi_limits[0] / 1000:.2g}-{pi_limits[1] / 1000:.2g} s' + time_lower = f'{pi_limits[0] / 1000:.1g}' + time_higher = f'{pi_limits[1] / 1000:.1g}' + + time_limit = f'{time_lower} s' if time_lower == time_higher else f'{time_lower}-{time_higher} s' - if pi_limits[2] == pi_limits[3]: # memory does not vary - memory_limit = f'{KiB_to_MB(pi_limits[2]):.2g} MB' + if pi_limits[2] < 1000000 / 1024: # lower memory limit is smaller than 1MB, display KiB + unit = 'KiB' + memory_lower = pi_limits[2] + memory_higher = pi_limits[3] else: - memory_limit = f'{KiB_to_MB(pi_limits[2]):.2g}-{KiB_to_MB(pi_limits[3]):.2g} MB' + unit = 'MB' + memory_lower = KiB_to_MB(pi_limits[2]) + memory_higher = KiB_to_MB(pi_limits[3]) + + memory_limit = f'{memory_lower} {unit}' if memory_lower == memory_higher else f'{memory_lower}-{memory_higher} {unit}' return time_limit, memory_limit From 86d1049f33b8c91f3e815872e2e64e7c8f5c8080 Mon Sep 17 00:00:00 2001 From: reivvax Date: Thu, 3 Apr 2025 23:13:21 +0200 Subject: [PATCH 09/12] Adjust style to display columns in proper manner, update tests --- oioioi/contests/static/common/contests.scss | 5 +++++ .../templates/contests/problems_list.html | 2 +- oioioi/contests/tests/tests.py | 18 +++++++++--------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/oioioi/contests/static/common/contests.scss b/oioioi/contests/static/common/contests.scss index 956d787e9..7929ded27 100644 --- a/oioioi/contests/static/common/contests.scss +++ b/oioioi/contests/static/common/contests.scss @@ -46,6 +46,10 @@ table.table-striped > tbody > tr.problemlist-subheader { margin-bottom: 4rem; } +.name-col { + width: 100%; +} + .limits-row { background-color: transparent !important; border-color: transparent !important; @@ -54,4 +58,5 @@ table.table-striped > tbody > tr.problemlist-subheader { .limits-row td { border: none; + white-space: nowrap; } diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index e56d41ab2..d5b2f404f 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -14,7 +14,7 @@

{% trans "Problems" %}

{% trans "Name" %}{% trans "Name" %} {% trans "Limits" %} diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 6e64d4c5c..57deedf4b 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -4581,20 +4581,20 @@ def test_stringify_limits(self): self.assertEqual(len(stringified['1']), 1) self.assertEqual(stringified['1'][0][0], '') self.assertEqual(stringified['1'][0][1], '1-2 s') - # self.assertEqual(stringified['1'][0][2], '1-2 MB') + self.assertEqual(stringified['1'][0][2], '1-2 MB') self.assertEqual(len(stringified['2']), 2) self.assertEqual(stringified['2'][0][0], 'C++:') self.assertEqual(stringified['2'][0][1], '0.5-2 s') - # self.assertEqual(stringified['2'][0][2], '0.51-1 MB') + self.assertEqual(stringified['2'][0][2], '500-1000 KiB') self.assertEqual(stringified['2'][1][0], 'Python:') self.assertEqual(stringified['2'][1][1], '1-3 s') - # self.assertEqual(stringified['2'][1][2], '1-3.1 MB') + self.assertEqual(stringified['2'][1][2], '1-3 MB') self.assertEqual(len(stringified['2']), 2) - self.assertEqual(stringified['2'][0][0], 'Default:') - self.assertEqual(stringified['2'][0][1], '1-2 s') - # self.assertEqual(stringified['2'][0][2], '1-2 MB') - self.assertEqual(stringified['2'][1][0], 'C++:') - self.assertEqual(stringified['2'][1][1], '0.5-2 s') - # self.assertEqual(stringified['2'][1][2], '0.51-1 MB') + self.assertEqual(stringified['3'][0][0], 'Default:') + self.assertEqual(stringified['3'][0][1], '1-2 s') + self.assertEqual(stringified['3'][0][2], '1-2 MB') + self.assertEqual(stringified['3'][1][0], 'C++:') + self.assertEqual(stringified['3'][1][1], '0.5-2 s') + self.assertEqual(stringified['3'][1][2], '500-1000 KiB') From 3f73f244348f7b951c121f418b5b77fb3a813373 Mon Sep 17 00:00:00 2001 From: reivvax Date: Sat, 5 Apr 2025 17:08:51 +0200 Subject: [PATCH 10/12] Bugfix --- oioioi/contests/controllers.py | 2 +- .../contests/templates/contests/problems_list.html | 12 ++++++------ oioioi/contests/views.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/oioioi/contests/controllers.py b/oioioi/contests/controllers.py index dc086d1ab..de73c97a5 100644 --- a/oioioi/contests/controllers.py +++ b/oioioi/contests/controllers.py @@ -597,7 +597,7 @@ def can_see_problems_limits(self, request): lvc = LimitsVisibilityConfig.objects.filter(contest=context.contest) if lvc.exists() and lvc[0].visible == 'YES': return True - elif lvc.exits() and lvc[0].visible == 'NO': + elif lvc.exists() and lvc[0].visible == 'NO': return False else: return self.default_can_see_problems_limits(request) diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index d5b2f404f..679d1ab20 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -59,9 +59,9 @@

{% trans "Problems" %}

{% if show_problems_limits %}
- - {% for row in problem_limits %} - {% if row %} + {% if problem_limits %} +
+ {% for row in problem_limits %} {% for entry in row %} {% endfor %} - {% endif %} - {% endfor %} -
@@ -69,9 +69,9 @@

{% trans "Problems" %}

+ {% endfor %} +
+ {% endif %} {% endif %} {% if show_submissions_limit %} diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index 220ed5515..7d83d90ce 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -165,7 +165,7 @@ def problems_list_view(request): pi, controller.can_see_statement(request, pi), controller.get_round_times(request, pi.round), - problems_limits[pi.pk], + problems_limits.get(pi.pk, None), # Because this view can be accessed by an anynomous user we can't # use `user=request.user` (it would cause TypeError). Surprisingly # using request.user.id is ok since for AnynomousUser id is set From fc35ed3c8e424532755877f774ba6e0f1f059ab4 Mon Sep 17 00:00:00 2001 From: reivvax Date: Sat, 5 Apr 2025 19:08:20 +0200 Subject: [PATCH 11/12] Further bugfix --- oioioi/contestexcl/tests.py | 1 + oioioi/contests/tests/__init__.py | 1 + oioioi/contests/views.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/oioioi/contestexcl/tests.py b/oioioi/contestexcl/tests.py index 88367d72c..99d05181a 100644 --- a/oioioi/contestexcl/tests.py +++ b/oioioi/contestexcl/tests.py @@ -180,6 +180,7 @@ def _modify_contestexcl( ('problemstatementconfig', 0, 0, 0, 1), ('rankingvisibilityconfig', 0, 0, 0, 1), ('registrationavailabilityconfig', 0, 0, 0, 1), + ('limitsvisibilityconfig', 0, 0, 0, 1), ('balloonsdeliveryaccessdata', 0, 0, 0, 1), ('statistics_config', 0, 0, 0, 1), ('exclusivenessconfig_set', len(excl_start_date_forms), 0, 0, 1000), diff --git a/oioioi/contests/tests/__init__.py b/oioioi/contests/tests/__init__.py index a5a5177d3..ec560d0c7 100644 --- a/oioioi/contests/tests/__init__.py +++ b/oioioi/contests/tests/__init__.py @@ -69,6 +69,7 @@ def make_empty_contest_formset(): ('problemstatementconfig', 0, 0, 0, 1), ('rankingvisibilityconfig', 0, 0, 0, 1), ('registrationavailabilityconfig', 0, 0, 0, 1), + ('limitsvisibilityconfig', 0, 0, 0, 1), ('balloonsdeliveryaccessdata', 1, 0, 0, 1), ('statistics_config', 1, 0, 0, 1), ('exclusivenessconfig_set', 0, 0, 0, 1000), diff --git a/oioioi/contests/views.py b/oioioi/contests/views.py index 7d83d90ce..1785030c8 100755 --- a/oioioi/contests/views.py +++ b/oioioi/contests/views.py @@ -193,8 +193,8 @@ def problems_list_view(request): key=lambda p: (p[2].get_key_for_comparison(), p[0].round.name, p[0].short_name), ) - show_submissions_limit = any([p[5] for p in problems_statements]) - show_submit_button = any([p[6] for p in problems_statements]) + show_submissions_limit = any([p[6] for p in problems_statements]) + show_submit_button = any([p[7] for p in problems_statements]) show_rounds = len(frozenset(pi.round_id for pi in problem_instances)) > 1 table_columns = 3 + int(show_problems_limits) + int(show_submissions_limit) + int(show_submit_button) From 0e87c99e5d923182d7299936ee7cf541bf8e0fd5 Mon Sep 17 00:00:00 2001 From: reivvax Date: Wed, 9 Apr 2025 17:52:13 +0200 Subject: [PATCH 12/12] Add more tests, small refactor --- oioioi/contests/static/common/contests.scss | 6 +- .../templates/contests/problems_list.html | 4 +- oioioi/contests/tests/tests.py | 206 ++++++++++++------ oioioi/contests/utils.py | 2 +- ...t_program_tests_and_languageoverrides.json | 26 ++- 5 files changed, 155 insertions(+), 89 deletions(-) diff --git a/oioioi/contests/static/common/contests.scss b/oioioi/contests/static/common/contests.scss index 7929ded27..b363dcebd 100644 --- a/oioioi/contests/static/common/contests.scss +++ b/oioioi/contests/static/common/contests.scss @@ -46,17 +46,17 @@ table.table-striped > tbody > tr.problemlist-subheader { margin-bottom: 4rem; } -.name-col { +.problem-name-column { width: 100%; } -.limits-row { +.problem-limits-row { background-color: transparent !important; border-color: transparent !important; white-space: nowrap; } -.limits-row td { +.problem-limits-row td { border: none; white-space: nowrap; } diff --git a/oioioi/contests/templates/contests/problems_list.html b/oioioi/contests/templates/contests/problems_list.html index 679d1ab20..f019a7e6d 100644 --- a/oioioi/contests/templates/contests/problems_list.html +++ b/oioioi/contests/templates/contests/problems_list.html @@ -14,7 +14,7 @@

{% trans "Problems" %}

- {% trans "Name" %} + {% trans "Name" %} {% if show_problems_limits %} {% trans "Limits" %} @@ -62,7 +62,7 @@

{% trans "Problems" %}

{% if problem_limits %} {% for row in problem_limits %} - + {% for entry in row %}
{{ entry }} diff --git a/oioioi/contests/tests/tests.py b/oioioi/contests/tests/tests.py index 57deedf4b..910a0d8c9 100755 --- a/oioioi/contests/tests/tests.py +++ b/oioioi/contests/tests/tests.py @@ -44,7 +44,7 @@ FilesMessage, SubmissionsMessage, SubmitMessage, - SubmissionMessage, + SubmissionMessage, LimitsVisibilityConfig, ) from oioioi.contests.scores import IntegerScore, ScoreValue from oioioi.contests.tests import make_empty_contest_formset @@ -568,60 +568,6 @@ def __init__(self, timestamp, contest): expected_order, ) - -class TestProblemLimitsFetching(TestCase): - fixtures = [ - 'test_contest', - 'test_program_tests_and_languageoverrides' - ] - - def test_problems_limits_for_contest_view(self): - contest = Contest.objects.get() - - class FakeRequest(object): - def __init__(self, timestamp, contest): - self.timestamp = timestamp - self.user = AnonymousUser() - self.contest = contest - - limits = contest.controller.get_problems_limits( - FakeRequest(datetime(2011, 1, 1, tzinfo=timezone.utc), contest) - ) - - # Test defaults - self.assertEqual(limits[1]['default'][0], 1000) - self.assertEqual(limits[1]['default'][1], 2000) - self.assertEqual(limits[1]['default'][2], 256000) - self.assertEqual(limits[1]['default'][3], 512000) - - self.assertEqual(limits[2]['default'][0], 1000) - self.assertEqual(limits[2]['default'][1], 4000) - self.assertEqual(limits[2]['default'][2], 128000) - self.assertEqual(limits[2]['default'][3], 1024000) - - # Test cpp - self.assertEqual(limits[1]['cpp'][0], 1000) - self.assertEqual(limits[1]['cpp'][1], 2000) - self.assertEqual(limits[1]['cpp'][2], 256000) - self.assertEqual(limits[1]['cpp'][3], 512000) - - self.assertEqual(limits[2]['cpp'][0], 500) - self.assertEqual(limits[2]['cpp'][1], 4000) - self.assertEqual(limits[2]['cpp'][2], 64000) - self.assertEqual(limits[2]['cpp'][3], 1024000) - - # Test python - self.assertEqual(limits[1]['py'][0], 1000) - self.assertEqual(limits[1]['py'][1], 2000) - self.assertEqual(limits[1]['py'][2], 256000) - self.assertEqual(limits[1]['py'][3], 512000) - - self.assertEqual(limits[2]['py'][0], 1000) - self.assertEqual(limits[2]['py'][1], 6000) - self.assertEqual(limits[2]['py'][2], 128000) - self.assertEqual(limits[2]['py'][3], 2048000) - - class TestContestViews(TestCase): fixtures = [ 'test_users', @@ -4555,7 +4501,80 @@ def test_score_badge(self): self.assertIn('badge-danger', self._get_badge_for_problem(response.content, 'zad3')) -class TestUtils(TestCase): +class TestProblemsLimits(TestCase): + fixtures = [ + 'test_users', + 'test_contest', + 'test_full_package', + 'test_problem_instance', + 'test_program_tests_and_languageoverrides' + ] + + def test_limits_visibility(self): + contest = Contest.objects.get() + url = reverse('problems_list', kwargs={'contest_id': contest.id}) + self.assertTrue(self.client.login(username='test_user')) + + # Limits are not displayed + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, 'Limits') + + # Display limits + _ = LimitsVisibilityConfig.objects.create(contest=contest, visible='YES') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Limits') + self.assertContains(response, 'C++:') + self.assertContains(response, 'Python:') + + + def test_problems_limits_for_contest_view(self): + contest = Contest.objects.get() + + class FakeRequest(object): + def __init__(self, timestamp, contest): + self.timestamp = timestamp + self.user = AnonymousUser() + self.contest = contest + + limits = contest.controller.get_problems_limits( + FakeRequest(datetime(2011, 1, 1, tzinfo=timezone.utc), contest) + ) + + # Test defaults + self.assertEqual(limits[2]['default'][0], 1000) + self.assertEqual(limits[2]['default'][1], 2000) + self.assertEqual(limits[2]['default'][2], 256000) + self.assertEqual(limits[2]['default'][3], 512000) + + self.assertEqual(limits[3]['default'][0], 1000) + self.assertEqual(limits[3]['default'][1], 4000) + self.assertEqual(limits[3]['default'][2], 128000) + self.assertEqual(limits[3]['default'][3], 1024000) + + # Test cpp + self.assertEqual(limits[2]['cpp'][0], 1000) + self.assertEqual(limits[2]['cpp'][1], 2000) + self.assertEqual(limits[2]['cpp'][2], 256000) + self.assertEqual(limits[2]['cpp'][3], 512000) + + self.assertEqual(limits[3]['cpp'][0], 500) + self.assertEqual(limits[3]['cpp'][1], 4000) + self.assertEqual(limits[3]['cpp'][2], 64000) + self.assertEqual(limits[3]['cpp'][3], 1024000) + + # Test python + self.assertEqual(limits[2]['py'][0], 1000) + self.assertEqual(limits[2]['py'][1], 2000) + self.assertEqual(limits[2]['py'][2], 256000) + self.assertEqual(limits[2]['py'][3], 512000) + + self.assertEqual(limits[3]['py'][0], 1000) + self.assertEqual(limits[3]['py'][1], 6000) + self.assertEqual(limits[3]['py'][2], 128000) + self.assertEqual(limits[3]['py'][3], 2048000) + def test_stringify_limits(self): raw_limits = { @@ -4565,36 +4584,81 @@ def test_stringify_limits(self): 'py': (1000, 2000, 1000, 2000), }, '2': { + 'default': (2000, 4000, 2000, 4000), + 'cpp': (1000, 3000, 1000, 4000), + 'py': (3000, 5000, 2000, 5000) + }, + '3': { 'default': (1000, 2000, 1000, 2000), 'cpp': (500, 2000, 500, 1000), 'py': (1000, 3000, 1000, 3000), }, - '3': { + '4': { 'default': (1000, 2000, 1000, 2000), - 'cpp': (500, 2000, 500, 1000), + 'cpp': (500, 2000, 2000, 3000), 'py': (1000, 2000, 1000, 2000), + }, + '5': { + 'default': (1000, 2000, 1000, 2000), + 'cpp': (1000, 2000, 1000, 2000), + 'py': (3000, 4000, 1000, 3000), + }, + '6': { + 'default': (1000, 2000, 500, 2000), + 'cpp': (100, 500, 100, 200), + 'py': (1000, 2000, 500, 2000), } } stringified = stringify_problems_limits(raw_limits) - print(stringified) + + # Test no overrides self.assertEqual(len(stringified['1']), 1) self.assertEqual(stringified['1'][0][0], '') self.assertEqual(stringified['1'][0][1], '1-2 s') self.assertEqual(stringified['1'][0][2], '1-2 MB') + # Test two simple overrides self.assertEqual(len(stringified['2']), 2) self.assertEqual(stringified['2'][0][0], 'C++:') - self.assertEqual(stringified['2'][0][1], '0.5-2 s') - self.assertEqual(stringified['2'][0][2], '500-1000 KiB') + self.assertEqual(stringified['2'][0][1], '1-3 s') + self.assertEqual(stringified['2'][0][2], '1-4 MB') self.assertEqual(stringified['2'][1][0], 'Python:') - self.assertEqual(stringified['2'][1][1], '1-3 s') - self.assertEqual(stringified['2'][1][2], '1-3 MB') - - self.assertEqual(len(stringified['2']), 2) - self.assertEqual(stringified['3'][0][0], 'Default:') - self.assertEqual(stringified['3'][0][1], '1-2 s') - self.assertEqual(stringified['3'][0][2], '1-2 MB') - self.assertEqual(stringified['3'][1][0], 'C++:') - self.assertEqual(stringified['3'][1][1], '0.5-2 s') - self.assertEqual(stringified['3'][1][2], '500-1000 KiB') + self.assertEqual(stringified['2'][1][1], '3-5 s') + self.assertEqual(stringified['2'][1][2], '2-5 MB') + + # Test two overrides, one with small memory limit + self.assertEqual(len(stringified['3']), 2) + self.assertEqual(stringified['3'][0][0], 'C++:') + self.assertEqual(stringified['3'][0][1], '0.5-2 s') + self.assertEqual(stringified['3'][0][2], '500-1000 KiB') + self.assertEqual(stringified['3'][1][0], 'Python:') + self.assertEqual(stringified['3'][1][1], '1-3 s') + self.assertEqual(stringified['3'][1][2], '1-3 MB') + + # Test one override, cpp + self.assertEqual(len(stringified['4']), 2) + self.assertEqual(stringified['4'][0][0], 'Default:') + self.assertEqual(stringified['4'][0][1], '1-2 s') + self.assertEqual(stringified['4'][0][2], '1-2 MB') + self.assertEqual(stringified['4'][1][0], 'C++:') + self.assertEqual(stringified['4'][1][1], '0.5-2 s') + self.assertEqual(stringified['4'][1][2], '2-3 MB') + + # Test one override, python + self.assertEqual(len(stringified['5']), 2) + self.assertEqual(stringified['5'][0][0], 'Default:') + self.assertEqual(stringified['5'][0][1], '1-2 s') + self.assertEqual(stringified['5'][0][2], '1-2 MB') + self.assertEqual(stringified['5'][1][0], 'Python:') + self.assertEqual(stringified['5'][1][1], '3-4 s') + self.assertEqual(stringified['5'][1][2], '1-3 MB') + + # Test small memory limits + self.assertEqual(len(stringified['6']), 2) + self.assertEqual(stringified['6'][0][0], 'Default:') + self.assertEqual(stringified['6'][0][1], '1-2 s') + self.assertEqual(stringified['6'][0][2], '500-2000 KiB') + self.assertEqual(stringified['6'][1][0], 'C++:') + self.assertEqual(stringified['6'][1][1], '0.1-0.5 s') + self.assertEqual(stringified['6'][1][2], '100-200 KiB') diff --git a/oioioi/contests/utils.py b/oioioi/contests/utils.py index 3c034d58c..340309a63 100755 --- a/oioioi/contests/utils.py +++ b/oioioi/contests/utils.py @@ -803,7 +803,7 @@ def stringify_problems_limits(raw_limits): - For mixed limits (one language differs): (('Default:', time_limit, memory_limit), language_limits) """ def KiB_to_MB(KiBs): - return KiBs * 1024 // 1000000 + return (KiBs * 1024) // 1000000 def format_limits(pi_limits): time_lower = f'{pi_limits[0] / 1000:.1g}' diff --git a/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json b/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json index d0dddaa29..086ea8423 100644 --- a/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json +++ b/oioioi/programs/fixtures/test_program_tests_and_languageoverrides.json @@ -5,7 +5,8 @@ "fields": { "legacy_name": "24_s2 1", "short_name": "24_s2 1", - "visibility": "PU" + "visibility": "PU", + "controller_name": "oioioi.sinolpack.controllers.SinolProblemController" } }, { @@ -14,26 +15,27 @@ "fields": { "legacy_name": "24_s2 2", "short_name": "24_s2 2", - "visibility": "PU" + "visibility": "PU", + "controller_name": "oioioi.sinolpack.controllers.SinolProblemController" } }, { - "pk": 1, + "pk": 2, "model": "contests.probleminstance", "fields": { "problem": 1, "round": 1, - "short_name": "zad1", + "short_name": "zad2", "contest": "c" } }, { - "pk": 2, + "pk": 3, "model": "contests.probleminstance", "fields": { "problem": 2, "round": 1, - "short_name": "zad2", + "short_name": "zad3", "contest": "c" } }, @@ -43,7 +45,7 @@ "model": "programs.test", "pk": 1, "fields": { - "problem_instance": 1, + "problem_instance": 2, "name": "test1", "kind": "NORMAL", "group": "g1", @@ -55,7 +57,7 @@ "model": "programs.test", "pk": 2, "fields": { - "problem_instance": 1, + "problem_instance": 2, "name": "test2", "kind": "NORMAL", "group": "g2", @@ -69,7 +71,7 @@ "model": "programs.test", "pk": 3, "fields": { - "problem_instance": 2, + "problem_instance": 3, "name": "test3", "kind": "NORMAL", "group": "g1", @@ -81,7 +83,7 @@ "model": "programs.test", "pk": 4, "fields": { - "problem_instance": 2, + "problem_instance": 3, "name": "test4", "kind": "NORMAL", "group": "g2", @@ -93,7 +95,7 @@ "model": "programs.test", "pk": 5, "fields": { - "problem_instance": 2, + "problem_instance": 3, "name": "test5", "kind": "NORMAL", "group": "g3", @@ -105,7 +107,7 @@ "model": "programs.test", "pk": 6, "fields": { - "problem_instance": 2, + "problem_instance": 3, "name": "test6", "kind": "NORMAL", "group": "g4",