From bb0bdab161cce146fd149ab944786434e7067a6e Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:03:05 +0700 Subject: [PATCH 01/22] Added problem import/export --- dmoj/settings.py | 6 + dmoj/urls.py | 6 + judge/admin/__init__.py | 5 +- judge/admin/problem.py | 19 +++ judge/migrations/0210_problem_export.py | 23 ++++ judge/models/__init__.py | 2 +- judge/models/problem.py | 27 +++- judge/views/problem_transfer.py | 173 ++++++++++++++++++++++++ templates/problem/import.html | 22 +++ 9 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 judge/migrations/0210_problem_export.py create mode 100644 judge/views/problem_transfer.py create mode 100644 templates/problem/import.html diff --git a/dmoj/settings.py b/dmoj/settings.py index c7649f6fa..d83f1176c 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -117,6 +117,11 @@ VNOJ_LONG_QUEUE_ALERT_THRESHOLD = 10 +# Transfer host +VNOJ_PROBLEM_IMPORT_HOST = 'https://oj.vnoi.info' +VNOJ_PROBLEM_IMPORT_SECRET = '' +VNOJ_PROBLEM_IMPORT_TIMEOUT = 5 # in seconds + CELERY_TIMEZONE = 'Asia/Ho_Chi_Minh' # Some problems have a lot of testcases, and each testcase @@ -326,6 +331,7 @@ 'judge.ProblemGroup', 'judge.ProblemType', 'judge.License', + 'judge.ProblemExportKey', ], }, { diff --git a/dmoj/urls.py b/dmoj/urls.py index c7e6d1c1b..183d81950 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -21,6 +21,7 @@ from judge.views.magazine import MagazinePage from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ problem_data_file, problem_init_view +from judge.views.problem_transfer import ProblemExportSelect2View, ProblemExportView, ProblemImportView from judge.views.register import ActivationView, RegistrationView from judge.views.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \ ContestUserSearchSelect2View, OrganizationSelect2View, OrganizationUserSelect2View, ProblemSelect2View, \ @@ -115,6 +116,11 @@ def paged_list_view(view, name): path('/suggest', problem.ProblemSuggest.as_view(), name='problem_suggest'), path('/create', problem.ProblemCreate.as_view(), name='problem_create'), path('/import-polygon', problem.ProblemImportPolygon.as_view(), name='problem_import_polygon'), + re_path('/export/(?P[a-zA-Z0-9_-]{48})/', include([ + path('', ProblemExportView.as_view(), name='problem_export'), + path('select/', ProblemExportSelect2View.as_view(), name='problem_export_select2_ajax'), + ])), + path('/import', ProblemImportView.as_view(), name='problem_import'), ])), path('problem/', include([ diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index c4e2751bd..9576921a8 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -7,7 +7,7 @@ from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin from judge.admin.interface import BlogPostAdmin, FlatPageAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin -from judge.admin.problem import ProblemAdmin +from judge.admin.problem import ProblemAdmin, ProblemExportKeyAdmin from judge.admin.profile import ProfileAdmin, UserAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.submission import SubmissionAdmin @@ -16,7 +16,7 @@ from judge.admin.ticket import TicketAdmin from judge.models import Badge, BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ - OrganizationRequest, Problem, ProblemGroup, ProblemType, Profile, Submission, Tag, \ + OrganizationRequest, Problem, ProblemExportKey, ProblemGroup, ProblemType, Profile, Submission, Tag, \ TagGroup, TagProblem, Ticket admin.site.register(BlogPost, BlogPostAdmin) @@ -37,6 +37,7 @@ admin.site.register(Organization, OrganizationAdmin) admin.site.register(OrganizationRequest, OrganizationRequestAdmin) admin.site.register(Problem, ProblemAdmin) +admin.site.register(ProblemExportKey, ProblemExportKeyAdmin) admin.site.register(ProblemGroup, ProblemGroupAdmin) admin.site.register(ProblemType, ProblemTypeAdmin) admin.site.register(Profile, ProfileAdmin) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 0c1b29688..08ee7b274 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -234,3 +234,22 @@ def construct_change_message(self, request, form, *args, **kwargs): if form.cleaned_data.get('change_message'): return form.cleaned_data['change_message'] return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) + + +class ProblemExportKeyForm(ModelForm): + pass + + +class ProblemExportKeyAdmin(VersionAdmin): + fieldsets = ( + (None, {'fields': ('name', 'remaining_uses')}), + (_('Description'), {'fields': ('description',)}), + ) + list_display = ['name', 'remaining_uses'] + search_fields = ('name', 'description') + + def save_model(self, request, obj, form, change): + super().save_model(request, obj, form, change) + if not change: + secret = obj.generate_secret() + self.message_user(request, gettext('Generated secret: %s') % secret) diff --git a/judge/migrations/0210_problem_export.py b/judge/migrations/0210_problem_export.py new file mode 100644 index 000000000..75a5ab679 --- /dev/null +++ b/judge/migrations/0210_problem_export.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-10-10 09:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0209_judge_add_tiers'), + ] + + operations = [ + migrations.CreateModel( + name='ProblemExportKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='name')), + ('token', models.CharField(max_length=64, verbose_name='token')), + ('remaining_uses', models.IntegerField(verbose_name='remaining uses')), + ('description', models.TextField(blank=True, verbose_name='description')), + ], + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 3644bc406..3ace951a1 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -6,7 +6,7 @@ ContestSubmission, ContestTag, Rating from judge.models.interface import BlogPost, BlogVote, MiscConfig, NavigationBar, validate_regex from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ - ProblemTranslation, ProblemType, Solution, SubmissionSourceAccess, TranslatedProblemQuerySet + ProblemExportKey, ProblemTranslation, ProblemType, Solution, SubmissionSourceAccess, TranslatedProblemQuerySet from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ problem_directory_file from judge.models.profile import Badge, Organization, OrganizationMonthlyUsage, OrganizationRequest, \ diff --git a/judge/models/problem.py b/judge/models/problem.py index 7a90824ae..8b8bbed07 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -1,5 +1,9 @@ +import base64 import errno +import hmac import json +import secrets +import struct from operator import attrgetter from django.conf import settings @@ -12,6 +16,7 @@ from django.db.models.functions import Coalesce from django.urls import reverse from django.utils import timezone +from django.utils.encoding import force_bytes from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ @@ -22,8 +27,8 @@ from judge.user_translations import gettext as user_gettext from judge.utils.url import get_absolute_pdf_url -__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification', 'License', - 'Solution', 'SubmissionSourceAccess', 'TranslatedProblemQuerySet'] +__all__ = ['ProblemGroup', 'ProblemType', 'Problem', 'ProblemTranslation', 'ProblemClarification', 'ProblemExportKey', + 'License', 'Solution', 'SubmissionSourceAccess', 'TranslatedProblemQuerySet'] def disallowed_characters_validator(text): @@ -697,3 +702,21 @@ class Meta: ) verbose_name = _('solution') verbose_name_plural = _('solutions') + + +class ProblemExportKey(models.Model): + name = models.CharField(max_length=100, verbose_name=_('name')) + token = models.CharField(max_length=64, verbose_name=_('token')) + remaining_uses = models.IntegerField(verbose_name=_('remaining uses')) + description = models.TextField(blank=True, verbose_name=_('description')) + + def __str__(self): + return self.name + + def generate_secret(self): + secret = secrets.token_bytes(32) + self.token = hmac.new(force_bytes(settings.SECRET_KEY), msg=secret, digestmod='sha256').hexdigest() + self.save(update_fields=['token']) + return base64.urlsafe_b64encode(struct.pack('>I32s', self.id, secret)).decode('utf-8') + + generate_secret.alters_data = True diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py new file mode 100644 index 000000000..06f8b3e15 --- /dev/null +++ b/judge/views/problem_transfer.py @@ -0,0 +1,173 @@ +import base64 +import hmac +import requests +import struct + +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.core.exceptions import PermissionDenied, ValidationError +from django.core.validators import RegexValidator +from django.db.models import Q +from django.forms import Form, CharField, PasswordInput, URLField +from django.http import Http404, JsonResponse, HttpResponseRedirect +from django.urls import reverse +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.utils.encoding import force_bytes +from django.utils.translation import gettext_lazy as _ +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import FormView +from django.views.generic.list import BaseListView +from requests.exceptions import HTTPError +from reversion import revisions + +from judge.models import Problem, ProblemExportKey, ProblemGroup, ProblemType, Language +from judge.utils.views import TitleMixin +from judge.widgets import HeavySelect2Widget + + +class ProblemExportMixin: + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + try: + id, secret = struct.unpack('>I32s', base64.urlsafe_b64decode(kwargs['secret'])) + self.transfer_key = ProblemExportKey.objects.get(id=id) + + # compare key + digest = hmac.new(force_bytes(settings.SECRET_KEY), msg=secret, digestmod='sha256').hexdigest() + if not hmac.compare_digest(digest, self.transfer_key.token): + raise HTTPError() + + except (ProblemExportKey.DoesNotExist, HTTPError): + raise Http404('Key not found') + + +class ProblemExportSelect2View(ProblemExportMixin, BaseListView): + paginate_by = 20 + + def get_queryset(self): + if self.transfer_key.remaining_uses <= 0: + return Problem.objects.none() + return Problem.get_public_problems().filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)) + + def get(self, request, *args, **kwargs): + self.request = request + self.term = kwargs.get('term', request.GET.get('term', '')) + self.object_list = self.get_queryset() + context = self.get_context_data() + + return JsonResponse({ + 'results': [ + { + 'text': obj.name, + 'id': obj.code, + } for obj in context['object_list']], + 'more': context['page_obj'].has_next(), + }) + + +@method_decorator(csrf_exempt, name='dispatch') +class ProblemExportView(ProblemExportMixin, View): + def get(self, request, *args, **kwargs): + return JsonResponse({ + 'name': self.transfer_key.name, + 'remaining_uses': self.transfer_key.remaining_uses, + }) + + def post(self, request, *args, **kwargs): + if self.transfer_key.remaining_uses <= 0: + raise PermissionDenied('No remaining uses') + + try: + code = request.POST.get('code', '') + if not code: + raise HTTPError() + problem = Problem.objects.get(code=code) + if not problem.is_accessible_by(AnonymousUser()): + raise HTTPError() + except (Problem.DoesNotExist, HTTPError): + raise Http404('Problem not found') + + self.transfer_key.remaining_uses -= 1 + self.transfer_key.save() + + return JsonResponse({ + 'code': problem.code, + 'name': problem.name, + 'description': problem.description, + 'time_limit': problem.time_limit, + 'memory_limit': problem.memory_limit, + 'points': problem.points, + 'partial': problem.partial, + 'short_circuit': problem.short_circuit, + }) + + +def get_problem_export_select_url(host=settings.VNOJ_PROBLEM_IMPORT_HOST, secret=settings.VNOJ_PROBLEM_IMPORT_SECRET): + return host + reverse('problem_export_select2_ajax', args=(secret,)) + + +def get_problem_export_url(host=settings.VNOJ_PROBLEM_IMPORT_HOST, secret=settings.VNOJ_PROBLEM_IMPORT_SECRET): + return host + reverse('problem_export', args=(secret,)) + + +class ProblemImportForm(Form): + problem = CharField(max_length=32, + validators=[RegexValidator('^[a-z0-9_]+$', _('Problem code must be ^[a-z0-9_]+$'))]) + code = CharField(max_length=32, validators=[RegexValidator('^[a-z0-9_]+$', _('Problem code must be ^[a-z0-9_]+$'))]) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['problem'].widget = HeavySelect2Widget(data_url=get_problem_export_select_url(), + attrs={'style': 'width: 100%'}) + + def clean_code(self): + code = self.cleaned_data['code'] + if Problem.objects.filter(code=code).exists(): + raise ValidationError(_('Problem with code already exists.')) + return code + + def clean(self): + key_info = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() + if not key_info: + self.add_error(None, _('Request timed out')) + elif key_info['remaining_uses'] <= 0: + self.add_error('secret', _('No remaining uses')) + + +class ProblemImportView(TitleMixin, FormView): + title = _('Import Problem') + template_name = 'problem/import.html' + form_class = ProblemImportForm + + def form_valid(self, form): + problem_info = requests.post(get_problem_export_url(), data={'code': form.cleaned_data['problem']}, timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() + + if not problem_info: + raise Http404('Request timed out') + problem = Problem() + problem.code = form.cleaned_data['code'] + problem.name = problem_info['name'] + problem.description = problem_info['description'] + problem.time_limit = problem_info['time_limit'] + problem.memory_limit = problem_info['memory_limit'] + problem.points = problem_info['points'] + problem.partial = problem_info['partial'] + problem.short_circuit = problem_info['short_circuit'] + problem.group = ProblemGroup.objects.order_by('id').first() # Uncategorized + problem.date = timezone.now() + with revisions.create_revision(atomic=True): + problem.save() + problem.allowed_languages.set(Language.objects.filter(include_in_problem=True)) + problem.types.set([ProblemType.objects.order_by('id').first()]) # Uncategorized + problem.curators.add(self.request.profile) + revisions.set_user(self.request.user) + revisions.set_comment(_('Imported from %s%s') % ( + settings.VNOJ_PROBLEM_IMPORT_HOST, reverse('problem_detail', args=(form.cleaned_data['problem'],)))) + return HttpResponseRedirect(reverse('problem_edit', args=(problem.code,))) + + def dispatch(self, request, *args, **kwargs): + if not request.user.is_superuser: + raise PermissionDenied() + return super().dispatch(request, *args, **kwargs) diff --git a/templates/problem/import.html b/templates/problem/import.html new file mode 100644 index 000000000..ee017f9df --- /dev/null +++ b/templates/problem/import.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block media %} + {{ form.media.css }} +{% endblock %} + +{% block js_media %} + {{ form.media.js }} +{% endblock %} + +{% block body %} +
+ {% if form.errors %} +
+ {{ form.non_field_errors() }} +
+ {% endif %} + {% csrf_token %} + {{ form.as_p() }} +

+
+{% endblock %} From b231adb8d959ccd2685bc7d71eb4de3eba984ba7 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:23:54 +0700 Subject: [PATCH 02/22] Enhanced import ui --- dmoj/settings.py | 3 +++ dmoj/urls.py | 11 +++++---- judge/template_context.py | 4 ++++ judge/views/problem_transfer.py | 22 +++++++++++------ templates/problem/import.html | 30 ++++++++++++++++-------- templates/problem/problem-list-tabs.html | 3 +++ 6 files changed, 51 insertions(+), 22 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index d83f1176c..440bb4099 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -118,6 +118,8 @@ VNOJ_LONG_QUEUE_ALERT_THRESHOLD = 10 # Transfer host +VNOJ_PROBLEM_ENABLE_IMPORT = False +VNOJ_PROBLEM_ENABLE_EXPORT = False VNOJ_PROBLEM_IMPORT_HOST = 'https://oj.vnoi.info' VNOJ_PROBLEM_IMPORT_SECRET = '' VNOJ_PROBLEM_IMPORT_TIMEOUT = 5 # in seconds @@ -502,6 +504,7 @@ 'judge.template_context.site_theme', 'judge.template_context.misc_config', 'judge.template_context.math_setting', + 'judge.template_context.site_setting', 'social_django.context_processors.backends', 'social_django.context_processors.login_redirect', ], diff --git a/dmoj/urls.py b/dmoj/urls.py index 183d81950..0e612cdc0 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -116,11 +116,6 @@ def paged_list_view(view, name): path('/suggest', problem.ProblemSuggest.as_view(), name='problem_suggest'), path('/create', problem.ProblemCreate.as_view(), name='problem_create'), path('/import-polygon', problem.ProblemImportPolygon.as_view(), name='problem_import_polygon'), - re_path('/export/(?P[a-zA-Z0-9_-]{48})/', include([ - path('', ProblemExportView.as_view(), name='problem_export'), - path('select/', ProblemExportSelect2View.as_view(), name='problem_export_select2_ajax'), - ])), - path('/import', ProblemImportView.as_view(), name='problem_import'), ])), path('problem/', include([ @@ -427,6 +422,12 @@ def paged_list_view(view, name): ])), path('magazine/', MagazinePage.as_view(), name='magazine'), + + re_path('^problem-export/(?P[a-zA-Z0-9_-]{48})', include([ + path('', ProblemExportView.as_view(), name='problem_export'), + path('/select', ProblemExportSelect2View.as_view(), name='problem_export_select2_ajax'), + ])), + path('problem-import', ProblemImportView.as_view(), name='problem_import'), ] favicon_paths = ['apple-touch-icon-180x180.png', 'apple-touch-icon-114x114.png', 'android-chrome-72x72.png', diff --git a/judge/template_context.py b/judge/template_context.py index e055cbdc7..24b549aee 100644 --- a/judge/template_context.py +++ b/judge/template_context.py @@ -106,3 +106,7 @@ def math_setting(request): if engine == 'auto': engine = 'mml' if bool(settings.MATHOID_URL) and caniuse.mathml == SUPPORT else 'jax' return {'MATH_ENGINE': engine, 'REQUIRE_JAX': engine == 'jax', 'caniuse': caniuse} + + +def site_setting(request): + return {'VNOJ_PROBLEM_ENABLE_IMPORT': settings.VNOJ_PROBLEM_ENABLE_IMPORT} \ No newline at end of file diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 06f8b3e15..f8e8cbbea 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -29,6 +29,8 @@ class ProblemExportMixin: def setup(self, request, *args, **kwargs): + if not settings.VNOJ_PROBLEM_ENABLE_EXPORT: + raise Http404() super().setup(request, *args, **kwargs) try: id, secret = struct.unpack('>I32s', base64.urlsafe_b64decode(kwargs['secret'])) @@ -115,18 +117,19 @@ def get_problem_export_url(host=settings.VNOJ_PROBLEM_IMPORT_HOST, secret=settin class ProblemImportForm(Form): problem = CharField(max_length=32, validators=[RegexValidator('^[a-z0-9_]+$', _('Problem code must be ^[a-z0-9_]+$'))]) - code = CharField(max_length=32, validators=[RegexValidator('^[a-z0-9_]+$', _('Problem code must be ^[a-z0-9_]+$'))]) + new_code = CharField(max_length=32, validators=[RegexValidator('^[a-z0-9_]+$', + _('Problem code must be ^[a-z0-9_]+$'))]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['problem'].widget = HeavySelect2Widget(data_url=get_problem_export_select_url(), attrs={'style': 'width: 100%'}) - def clean_code(self): - code = self.cleaned_data['code'] - if Problem.objects.filter(code=code).exists(): + def clean_new_code(self): + new_code = self.cleaned_data['new_code'] + if Problem.objects.filter(code=new_code).exists(): raise ValidationError(_('Problem with code already exists.')) - return code + return new_code def clean(self): key_info = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() @@ -142,12 +145,14 @@ class ProblemImportView(TitleMixin, FormView): form_class = ProblemImportForm def form_valid(self, form): - problem_info = requests.post(get_problem_export_url(), data={'code': form.cleaned_data['problem']}, timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() + problem_info = requests.post(get_problem_export_url(), + data={'code': form.cleaned_data['problem']}, + timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() if not problem_info: raise Http404('Request timed out') problem = Problem() - problem.code = form.cleaned_data['code'] + problem.code = form.cleaned_data['new_code'] problem.name = problem_info['name'] problem.description = problem_info['description'] problem.time_limit = problem_info['time_limit'] @@ -157,6 +162,7 @@ def form_valid(self, form): problem.short_circuit = problem_info['short_circuit'] problem.group = ProblemGroup.objects.order_by('id').first() # Uncategorized problem.date = timezone.now() + problem.is_manually_managed = True with revisions.create_revision(atomic=True): problem.save() problem.allowed_languages.set(Language.objects.filter(include_in_problem=True)) @@ -168,6 +174,8 @@ def form_valid(self, form): return HttpResponseRedirect(reverse('problem_edit', args=(problem.code,))) def dispatch(self, request, *args, **kwargs): + if not settings.VNOJ_PROBLEM_ENABLE_IMPORT: + raise Http404() if not request.user.is_superuser: raise PermissionDenied() return super().dispatch(request, *args, **kwargs) diff --git a/templates/problem/import.html b/templates/problem/import.html index ee017f9df..2facdee14 100644 --- a/templates/problem/import.html +++ b/templates/problem/import.html @@ -2,21 +2,31 @@ {% block media %} {{ form.media.css }} + {% endblock %} {% block js_media %} {{ form.media.js }} {% endblock %} +{% block title_ruler %}{% endblock %} + +{% block title_row %} + {% set tab = 'import' %} + {% include "problem/problem-list-tabs.html" %} +{% endblock %} + {% block body %} -
- {% if form.errors %} -
- {{ form.non_field_errors() }} -
- {% endif %} - {% csrf_token %} - {{ form.as_p() }} -

-
+
+
+ {% if form.non_field_errors() %} +
+ {{ form.non_field_errors() }} +
+ {% endif %} + {% csrf_token %} + {{ form.as_table() }}
+

+
+
{% endblock %} diff --git a/templates/problem/problem-list-tabs.html b/templates/problem/problem-list-tabs.html index cc415067d..bd72bea87 100644 --- a/templates/problem/problem-list-tabs.html +++ b/templates/problem/problem-list-tabs.html @@ -12,6 +12,9 @@ {{ make_tab('suggest', 'fa-list', url('problem_suggest_list'), _('Suggest')) }} {% endif %} {% if request.user.is_superuser %} + {% if VNOJ_PROBLEM_ENABLE_IMPORT %} + {{ make_tab('import', 'fa-cloud-download', url('problem_import'), _('Import')) }} + {% endif %} {{ make_tab('admin', 'fa-edit', url('admin:judge_problem_changelist'), _('Admin')) }} {% endif %} {% endblock %} From dd5c5d6f33eaca7fe1741e88ac83919786bf93f1 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:27:36 +0700 Subject: [PATCH 03/22] Fix lint --- judge/models/__init__.py | 4 ++-- judge/template_context.py | 2 +- judge/views/problem_transfer.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 3ace951a1..b01768483 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -5,8 +5,8 @@ from judge.models.contest import Contest, ContestAnnouncement, ContestMoss, ContestParticipation, ContestProblem, \ ContestSubmission, ContestTag, Rating from judge.models.interface import BlogPost, BlogVote, MiscConfig, NavigationBar, validate_regex -from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemGroup, \ - ProblemExportKey, ProblemTranslation, ProblemType, Solution, SubmissionSourceAccess, TranslatedProblemQuerySet +from judge.models.problem import LanguageLimit, License, Problem, ProblemClarification, ProblemExportKey, \ + ProblemGroup, ProblemTranslation, ProblemType, Solution, SubmissionSourceAccess, TranslatedProblemQuerySet from judge.models.problem_data import CHECKERS, ProblemData, ProblemTestCase, problem_data_storage, \ problem_directory_file from judge.models.profile import Badge, Organization, OrganizationMonthlyUsage, OrganizationRequest, \ diff --git a/judge/template_context.py b/judge/template_context.py index 24b549aee..42c4383cd 100644 --- a/judge/template_context.py +++ b/judge/template_context.py @@ -109,4 +109,4 @@ def math_setting(request): def site_setting(request): - return {'VNOJ_PROBLEM_ENABLE_IMPORT': settings.VNOJ_PROBLEM_ENABLE_IMPORT} \ No newline at end of file + return {'VNOJ_PROBLEM_ENABLE_IMPORT': settings.VNOJ_PROBLEM_ENABLE_IMPORT} diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index f8e8cbbea..2b7b0decb 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -1,15 +1,15 @@ import base64 import hmac -import requests import struct +import requests from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import RegexValidator from django.db.models import Q -from django.forms import Form, CharField, PasswordInput, URLField -from django.http import Http404, JsonResponse, HttpResponseRedirect +from django.forms import CharField, Form +from django.http import Http404, HttpResponseRedirect, JsonResponse from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator @@ -22,7 +22,7 @@ from requests.exceptions import HTTPError from reversion import revisions -from judge.models import Problem, ProblemExportKey, ProblemGroup, ProblemType, Language +from judge.models import Language, Problem, ProblemExportKey, ProblemGroup, ProblemType from judge.utils.views import TitleMixin from judge.widgets import HeavySelect2Widget From 4108963babbcb929556ded84458b392d7f764340 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:42:12 +0700 Subject: [PATCH 04/22] Remove redundant code --- judge/admin/problem.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 08ee7b274..c12772424 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -236,10 +236,6 @@ def construct_change_message(self, request, form, *args, **kwargs): return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) -class ProblemExportKeyForm(ModelForm): - pass - - class ProblemExportKeyAdmin(VersionAdmin): fieldsets = ( (None, {'fields': ('name', 'remaining_uses')}), From 43cda848a9f64182fd2f2b7e66d9f05a7d3683f2 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:30:13 +0700 Subject: [PATCH 05/22] Add CORS allow list to the export API --- dmoj/settings.py | 3 +++ judge/signals.py | 8 ++++++++ requirements.txt | 1 + 3 files changed, 12 insertions(+) diff --git a/dmoj/settings.py b/dmoj/settings.py index 440bb4099..24b7754c6 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -28,6 +28,7 @@ DEBUG = True ALLOWED_HOSTS = [] +CORS_ALLOWED_ORIGINS = [] CSRF_FAILURE_VIEW = 'judge.views.widgets.csrf_failure' @@ -429,9 +430,11 @@ 'martor', 'adminsortable2', 'django_cleanup.apps.CleanupConfig', + 'corsheaders', ) MIDDLEWARE = ( + 'corsheaders.middleware.CorsMiddleware', 'judge.middleware.ShortCircuitMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/judge/signals.py b/judge/signals.py index 87c2f8386..738c2aa3c 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -2,6 +2,7 @@ import os from typing import Optional +from corsheaders.signals import check_request_enabled from django.conf import settings from django.contrib.flatpages.models import FlatPage from django.core.cache import cache @@ -203,3 +204,10 @@ def registration_user_registered(sender, user, request, **kwargs): with transaction.atomic(): user.save() profile.save() + + +def cors_allow_problem_export_api(sender, request, **kwargs): + return request.path.startswith('/problem-export/') + + +check_request_enabled.connect(cors_allow_problem_export_api) diff --git a/requirements.txt b/requirements.txt index 382152e03..dc7e6500c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Django>=3.2,<4 django-cleanup django_compressor>=3 +django-cors-headers django-mptt>=0.13 django-registration-redux>=2.10 django-reversion>=3.0.5,<4 From 9d6c7bdd8797ed412fca3ab45a83b7b28ecffabe Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Fri, 11 Oct 2024 07:23:25 +0700 Subject: [PATCH 06/22] Removal of empty file --- judge/balancer/judge_list.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 judge/balancer/judge_list.py diff --git a/judge/balancer/judge_list.py b/judge/balancer/judge_list.py deleted file mode 100644 index e69de29bb..000000000 From 25d431e9de86de07b0e6c71169068115b9d59a72 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:45:59 +0700 Subject: [PATCH 07/22] Add judge code --- dmoj/settings.py | 1 + judge/bridge/daemon.py | 10 ++-- judge/bridge/monitor.py | 68 +++++++++++++++++++++++-- judge/management/commands/runbridged.py | 15 +++--- judge/migrations/0210_problem_export.py | 17 +++++++ judge/models/problem.py | 3 ++ judge/views/problem_transfer.py | 1 + 7 files changed, 100 insertions(+), 15 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index 24b7754c6..e6f9a278a 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -123,6 +123,7 @@ VNOJ_PROBLEM_ENABLE_EXPORT = False VNOJ_PROBLEM_IMPORT_HOST = 'https://oj.vnoi.info' VNOJ_PROBLEM_IMPORT_SECRET = '' +VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX = 'vnoj/' VNOJ_PROBLEM_IMPORT_TIMEOUT = 5 # in seconds CELERY_TIMEZONE = 'Asia/Ho_Chi_Minh' diff --git a/judge/bridge/daemon.py b/judge/bridge/daemon.py index 00ee09464..480e7cba6 100644 --- a/judge/bridge/daemon.py +++ b/judge/bridge/daemon.py @@ -18,20 +18,22 @@ def reset_judges(): Judge.objects.update(online=False, ping=None, load=None) -def judge_daemon(run_monitor=False, problem_storage_globs=None): +def judge_daemon(config): reset_judges() Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \ .update(status='IE', result='IE', error=None) judges = JudgeList() monitor = None - if run_monitor: + if 'run_monitor' in config: from judge.bridge.monitor import Monitor - monitor = Monitor(judges, problem_storage_globs or []) + problem_storage_globs = [(entry['storage_namespaces'], entry['problem_storage_globs']) + for entry in config['run_monitor']] + monitor = Monitor(judges, problem_storage_globs) judge_server = Server( settings.BRIDGED_JUDGE_ADDRESS, - partial(JudgeHandler, judges=judges, ignore_problems_packet=run_monitor), + partial(JudgeHandler, judges=judges, ignore_problems_packet=('run_monitor' in config)), ) django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)) diff --git a/judge/bridge/monitor.py b/judge/bridge/monitor.py index 8006e21c8..e772affdb 100644 --- a/judge/bridge/monitor.py +++ b/judge/bridge/monitor.py @@ -1,11 +1,14 @@ +from contextlib import closing import glob import logging import os import threading import time from pathlib import Path +from urllib.request import urlopen from django import db +from http.server import BaseHTTPRequestHandler, HTTPServer from judge.models import Problem @@ -27,6 +30,27 @@ logger = logging.getLogger('judge.monitor') +class JudgeControlRequestHandler(BaseHTTPRequestHandler): + signal = None + + def update_problems(self): + if self.signal is not None: + self.signal.set() + + def do_POST(self): + if self.path == '/update/problems': + logger.info('Problem update requested') + self.update_problems() + self.send_response(200) + self.end_headers() + self.wfile.write(b'As you wish.') + return + self.send_error(404) + + def do_GET(self): + self.send_error(404) + + def _ensure_connection(): db.connection.close_if_unusable_or_obsolete() @@ -59,7 +83,7 @@ def on_any_event(self, event): class Monitor: - def __init__(self, judges, problem_globs): + def __init__(self, judges, problem_globs, api_listen=None, update_pings=None): if not has_watchdog_installed: raise ImportError('watchdog is not installed') @@ -68,33 +92,64 @@ def __init__(self, judges, problem_globs): self.updater_exit = False self.updater_signal = threading.Event() + self.propagation_signal = threading.Event() self.updater = threading.Thread(target=self.updater_thread) + self.propagator = threading.Thread(target=self.propagate_update_signal) + + self.update_pings = update_pings or [] + + if api_listen: + class Handler(JudgeControlRequestHandler): + signal = self.updater_signal + + api_server = HTTPServer(api_listen, Handler) + self.api_server_thread = threading.Thread(target=api_server.serve_forever) self._handler = SendProblemsHandler(self.updater_signal) self._observer = Observer() - for root in set(map(find_glob_root, problem_globs)): + for _, dir_glob in problem_globs: + root = find_glob_root(dir_glob) self._observer.schedule(self._handler, root, recursive=True) logger.info('Scheduled for monitoring: %s', root) def update_supported_problems(self): problems = [] - for dir_glob in self.problem_globs: + for storage_namespace, dir_glob in self.problem_globs: for problem_config in glob.iglob(os.path.join(dir_glob, 'init.yml'), recursive=True): if os.access(problem_config, os.R_OK): problem_dir = os.path.dirname(problem_config) problem = os.path.basename(problem_dir) - problems.append(problem) + if storage_namespace: + problems.append(storage_namespace + '/' + problem) + else: + problems.append(problem) problems = set(problems) _ensure_connection() - problem_ids = list(Problem.objects.filter(code__in=list(problems)).values_list('id', flat=True)) + problem_ids = list(Problem.objects.filter(judge_code__in=list(problems)).values_list('id', flat=True)) self.judges.update_problems_all(problems, problem_ids) + def propagate_update_signal(self): + while True: + self.propagation_signal.wait() + self.propagation_signal.clear() + if self.updater_exit: + return + + for url in self.update_pings: + logger.info('Pinging for problem update: %s', url) + try: + with closing(urlopen(url, data='')) as f: + f.read() + except Exception: + logger.exception('Failed to ping for problem update: %s', url) + def updater_thread(self) -> None: while True: self.updater_signal.wait() self.updater_signal.clear() + self.propagation_signal.set() if self.updater_exit: return @@ -106,8 +161,11 @@ def updater_thread(self) -> None: def start(self): self.updater.start() + self.propagator.start() self.updater_signal.set() try: + if hasattr(self, 'api_server_thread'): + self.api_server_thread.start() self._observer.start() except OSError: logger.exception('Failed to start problem monitor.') diff --git a/judge/management/commands/runbridged.py b/judge/management/commands/runbridged.py index e7267f375..dd560b720 100644 --- a/judge/management/commands/runbridged.py +++ b/judge/management/commands/runbridged.py @@ -1,14 +1,17 @@ +import yaml from django.core.management.base import BaseCommand from judge.bridge.daemon import judge_daemon class Command(BaseCommand): - def add_arguments(self, parser): - parser.add_argument('--monitor', action='store_true', default=False, - help='if specified, run a monitor to automatically update problems') - parser.add_argument('--problem-storage-globs', nargs='*', default=[], - help='globs to monitor for problem updates') + def add_arguments(self, parser) -> None: + parser.add_argument('-c', '--config', type=str, help='file to load bridged configurations from') def handle(self, *args, **options): - judge_daemon(options['monitor'], options['problem_storage_globs']) + if options['config']: + with open(options['config'], 'r') as f: + config = yaml.safe_load(f) + else: + config = {} + judge_daemon(config) diff --git a/judge/migrations/0210_problem_export.py b/judge/migrations/0210_problem_export.py index 75a5ab679..7c20f6554 100644 --- a/judge/migrations/0210_problem_export.py +++ b/judge/migrations/0210_problem_export.py @@ -1,6 +1,12 @@ # Generated by Django 3.2.25 on 2024-10-10 09:25 from django.db import migrations, models +from django.db.models import F + + +def populate_judge_code(apps, schema_editor): + Problem = apps.get_model('judge', 'Problem') + Problem.objects.update(judge_code=F('code')) class Migration(migrations.Migration): @@ -20,4 +26,15 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, verbose_name='description')), ], ), + migrations.AddField( + model_name='problem', + name='judge_code', + field=models.CharField(blank=True, max_length=100, verbose_name='judge code'), + ), + migrations.RunPython(populate_judge_code, migrations.RunPython.noop, atomic=True), + migrations.AlterField( + model_name='problem', + name='judge_code', + field=models.CharField(max_length=100, verbose_name='judge code'), + ), ] diff --git a/judge/models/problem.py b/judge/models/problem.py index 8b8bbed07..072b93e0b 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -190,6 +190,7 @@ class Problem(models.Model): is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False) is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False, help_text=_('Whether judges should be allowed to manage data or not.')) + judge_code = models.CharField(max_length=100, verbose_name=_('judge code')) date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True, help_text=_( "Doesn't have the magic ability to auto-publish due to backward compatibility.")) @@ -573,6 +574,8 @@ def io_method(self): return {'method': 'standard'} def save(self, *args, **kwargs): + if not self.judge_code: + self.judge_code = self.code is_clone = kwargs.pop('is_clone', False) # if short_circuit = true the judge will stop judging # as soon as the submission failed a test case diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 2b7b0decb..19b12cdb2 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -153,6 +153,7 @@ def form_valid(self, form): raise Http404('Request timed out') problem = Problem() problem.code = form.cleaned_data['new_code'] + problem.judge_code = settings.VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX + problem.code problem.name = problem_info['name'] problem.description = problem_info['description'] problem.time_limit = problem_info['time_limit'] From c42cdf692120e99a680a812ba2662eef3fdc1e60 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 00:03:37 +0700 Subject: [PATCH 08/22] Allow judge_code to be empty --- judge/bridge/daemon.py | 4 +--- judge/bridge/judge_handler.py | 5 +++++ judge/bridge/monitor.py | 20 +++++++++++++------- judge/judgeapi.py | 2 +- judge/migrations/0210_problem_export.py | 12 ------------ judge/models/problem.py | 4 +--- judge/views/problem_transfer.py | 3 ++- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/judge/bridge/daemon.py b/judge/bridge/daemon.py index 480e7cba6..ca0c87c88 100644 --- a/judge/bridge/daemon.py +++ b/judge/bridge/daemon.py @@ -27,9 +27,7 @@ def judge_daemon(config): monitor = None if 'run_monitor' in config: from judge.bridge.monitor import Monitor - problem_storage_globs = [(entry['storage_namespaces'], entry['problem_storage_globs']) - for entry in config['run_monitor']] - monitor = Monitor(judges, problem_storage_globs) + monitor = Monitor(judges, **config) judge_server = Server( settings.BRIDGED_JUDGE_ADDRESS, diff --git a/judge/bridge/judge_handler.py b/judge/bridge/judge_handler.py index f70eb9c45..25a739418 100644 --- a/judge/bridge/judge_handler.py +++ b/judge/bridge/judge_handler.py @@ -237,10 +237,15 @@ def submit(self, id, problem, language, source): data = self.get_related_submission_data(id) self._working = id self._no_response_job = threading.Timer(20, self._kill_if_no_response) + if '/' in problem: + storage_namespace, problem = problem.split('/') + else: + storage_namespace = '' self.send({ 'name': 'submission-request', 'submission-id': id, 'problem-id': problem, + 'storage-namespace': storage_namespace, 'language': language, 'source': source if not data.file_only else get_absolute_submission_file_url(source), 'time-limit': data.time, diff --git a/judge/bridge/monitor.py b/judge/bridge/monitor.py index e772affdb..ba55b895e 100644 --- a/judge/bridge/monitor.py +++ b/judge/bridge/monitor.py @@ -1,14 +1,15 @@ -from contextlib import closing import glob import logging import os import threading import time +from contextlib import closing +from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from urllib.request import urlopen from django import db -from http.server import BaseHTTPRequestHandler, HTTPServer +from django.db.models import Q from judge.models import Problem @@ -83,12 +84,13 @@ def on_any_event(self, event): class Monitor: - def __init__(self, judges, problem_globs, api_listen=None, update_pings=None): + def __init__(self, judges, **config): if not has_watchdog_installed: raise ImportError('watchdog is not installed') self.judges = judges - self.problem_globs = problem_globs + self.problem_globs = [(entry['storage_namespaces'], entry['problem_storage_globs']) + for entry in config['run_monitor']] self.updater_exit = False self.updater_signal = threading.Event() @@ -96,9 +98,12 @@ def __init__(self, judges, problem_globs, api_listen=None, update_pings=None): self.updater = threading.Thread(target=self.updater_thread) self.propagator = threading.Thread(target=self.propagate_update_signal) - self.update_pings = update_pings or [] + self.update_pings = config.get('update_pings') or [] + api_listen = config.get('api_listen') if api_listen: + api_listen = (api_listen['host'], api_listen['port']) + class Handler(JudgeControlRequestHandler): signal = self.updater_signal @@ -108,7 +113,7 @@ class Handler(JudgeControlRequestHandler): self._handler = SendProblemsHandler(self.updater_signal) self._observer = Observer() - for _, dir_glob in problem_globs: + for _, dir_glob in self.problem_globs: root = find_glob_root(dir_glob) self._observer.schedule(self._handler, root, recursive=True) logger.info('Scheduled for monitoring: %s', root) @@ -127,7 +132,8 @@ def update_supported_problems(self): problems = set(problems) _ensure_connection() - problem_ids = list(Problem.objects.filter(judge_code__in=list(problems)).values_list('id', flat=True)) + problem_ids = list(Problem.objects.filter(Q(code__in=list(problems)) | Q(judge_code__in=list(problems))) + .values_list('id', flat=True)) self.judges.update_problems_all(problems, problem_ids) def propagate_update_signal(self): diff --git a/judge/judgeapi.py b/judge/judgeapi.py index 1bdc14c54..18acad6a8 100644 --- a/judge/judgeapi.py +++ b/judge/judgeapi.py @@ -94,7 +94,7 @@ def judge_submission(submission, rejudge=False, batch_rejudge=False, judge_id=No response = judge_request({ 'name': 'submission-request', 'submission-id': submission.id, - 'problem-id': submission.problem.code, + 'problem-id': submission.problem.judge_code or submission.problem.code, 'language': submission.language.key, 'source': submission.source.source, 'judge-id': judge_id, diff --git a/judge/migrations/0210_problem_export.py b/judge/migrations/0210_problem_export.py index 7c20f6554..bd97f0c82 100644 --- a/judge/migrations/0210_problem_export.py +++ b/judge/migrations/0210_problem_export.py @@ -1,12 +1,6 @@ # Generated by Django 3.2.25 on 2024-10-10 09:25 from django.db import migrations, models -from django.db.models import F - - -def populate_judge_code(apps, schema_editor): - Problem = apps.get_model('judge', 'Problem') - Problem.objects.update(judge_code=F('code')) class Migration(migrations.Migration): @@ -31,10 +25,4 @@ class Migration(migrations.Migration): name='judge_code', field=models.CharField(blank=True, max_length=100, verbose_name='judge code'), ), - migrations.RunPython(populate_judge_code, migrations.RunPython.noop, atomic=True), - migrations.AlterField( - model_name='problem', - name='judge_code', - field=models.CharField(max_length=100, verbose_name='judge code'), - ), ] diff --git a/judge/models/problem.py b/judge/models/problem.py index 072b93e0b..664a4e231 100644 --- a/judge/models/problem.py +++ b/judge/models/problem.py @@ -190,7 +190,7 @@ class Problem(models.Model): is_public = models.BooleanField(verbose_name=_('publicly visible'), db_index=True, default=False) is_manually_managed = models.BooleanField(verbose_name=_('manually managed'), db_index=True, default=False, help_text=_('Whether judges should be allowed to manage data or not.')) - judge_code = models.CharField(max_length=100, verbose_name=_('judge code')) + judge_code = models.CharField(max_length=100, verbose_name=_('judge code'), blank=True) date = models.DateTimeField(verbose_name=_('date of publishing'), null=True, blank=True, db_index=True, help_text=_( "Doesn't have the magic ability to auto-publish due to backward compatibility.")) @@ -574,8 +574,6 @@ def io_method(self): return {'method': 'standard'} def save(self, *args, **kwargs): - if not self.judge_code: - self.judge_code = self.code is_clone = kwargs.pop('is_clone', False) # if short_circuit = true the judge will stop judging # as soon as the submission failed a test case diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 19b12cdb2..23b164b39 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -153,7 +153,8 @@ def form_valid(self, form): raise Http404('Request timed out') problem = Problem() problem.code = form.cleaned_data['new_code'] - problem.judge_code = settings.VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX + problem.code + # Use the exported code + problem.judge_code = settings.VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX + problem_info['code'] problem.name = problem_info['name'] problem.description = problem_info['description'] problem.time_limit = problem_info['time_limit'] From 8ac66c9017f83df23ed1c95cdbad2668e63b3003 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 01:59:31 +0700 Subject: [PATCH 09/22] Reload monitor on problem update --- dmoj/settings.py | 1 + judge/bridge/monitor.py | 6 ++- judge/views/problem_transfer.py | 87 +++++++++++++++++++++------------ 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index e6f9a278a..6def7e7dd 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -681,6 +681,7 @@ BRIDGED_JUDGE_PROXIES = None BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)] BRIDGED_DJANGO_CONNECT = None +BRIDGED_MONITOR_UPDATE_URL = 'http://localhost:9990/update/problems' # Event Server configuration EVENT_DAEMON_USE = False diff --git a/judge/bridge/monitor.py b/judge/bridge/monitor.py index ba55b895e..cdfd36401 100644 --- a/judge/bridge/monitor.py +++ b/judge/bridge/monitor.py @@ -74,13 +74,15 @@ class SendProblemsHandler(FileSystemEventHandler): EVENT_TYPE_CREATED, ) - def __init__(self, signal): + def __init__(self, signal, propagation_signal): self.signal = signal + self.propagation_signal = propagation_signal def on_any_event(self, event): if event.event_type not in self.ALLOWED_EVENT_TYPES: return self.signal.set() + self.propagation_signal.set() class Monitor: @@ -110,7 +112,7 @@ class Handler(JudgeControlRequestHandler): api_server = HTTPServer(api_listen, Handler) self.api_server_thread = threading.Thread(target=api_server.serve_forever) - self._handler = SendProblemsHandler(self.updater_signal) + self._handler = SendProblemsHandler(self.updater_signal, self.propagation_signal) self._observer = Observer() for _, dir_glob in self.problem_globs: diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 23b164b39..9c026ea7c 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -1,15 +1,19 @@ import base64 import hmac +import logging import struct +from contextlib import closing +from urllib.request import urlopen import requests +from celery import shared_task from django.conf import settings -from django.contrib.auth.models import AnonymousUser +from django.contrib.auth.models import AnonymousUser, User from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import RegexValidator from django.db.models import Q from django.forms import CharField, Form -from django.http import Http404, HttpResponseRedirect, JsonResponse +from django.http import Http404, JsonResponse from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator @@ -23,10 +27,14 @@ from reversion import revisions from judge.models import Language, Problem, ProblemExportKey, ProblemGroup, ProblemType +from judge.utils.celery import redirect_to_task_status from judge.utils.views import TitleMixin from judge.widgets import HeavySelect2Widget +logger = logging.getLogger('judge.problem.transfer') + + class ProblemExportMixin: def setup(self, request, *args, **kwargs): if not settings.VNOJ_PROBLEM_ENABLE_EXPORT: @@ -139,41 +147,58 @@ def clean(self): self.add_error('secret', _('No remaining uses')) +@shared_task(bind=True) +def import_problem(self, user_id, problem, new_code): + old_code = problem + problem_info = requests.post(get_problem_export_url(), + data={'code': old_code}, + timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() + + if not problem_info: + raise Http404() + problem = Problem() + problem.code = new_code + # Use the exported code + problem.judge_code = settings.VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX + problem_info['code'] + problem.name = problem_info['name'] + problem.description = problem_info['description'] + problem.time_limit = problem_info['time_limit'] + problem.memory_limit = problem_info['memory_limit'] + problem.points = problem_info['points'] + problem.partial = problem_info['partial'] + problem.short_circuit = problem_info['short_circuit'] + problem.group = ProblemGroup.objects.order_by('id').first() # Uncategorized + problem.date = timezone.now() + problem.is_manually_managed = True + with revisions.create_revision(atomic=True): + problem.save() + problem.allowed_languages.set(Language.objects.filter(include_in_problem=True)) + problem.types.set([ProblemType.objects.order_by('id').first()]) # Uncategorized + user = User.objects.get(id=user_id) + problem.curators.add(user.profile) + revisions.set_user(user) + revisions.set_comment(_('Imported from %s%s') % ( + settings.VNOJ_PROBLEM_IMPORT_HOST, reverse('problem_detail', args=(old_code,)))) + url = settings.BRIDGED_MONITOR_UPDATE_URL + logger.info('Pinging for problem update: %s', url) + try: + with closing(urlopen(url, data=b'')) as f: + f.read() + except Exception: + logger.exception('Failed to ping for problem update: %s', url) + + class ProblemImportView(TitleMixin, FormView): title = _('Import Problem') template_name = 'problem/import.html' form_class = ProblemImportForm def form_valid(self, form): - problem_info = requests.post(get_problem_export_url(), - data={'code': form.cleaned_data['problem']}, - timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() - - if not problem_info: - raise Http404('Request timed out') - problem = Problem() - problem.code = form.cleaned_data['new_code'] - # Use the exported code - problem.judge_code = settings.VNOJ_PROBLEM_IMPORT_JUDGE_PREFIX + problem_info['code'] - problem.name = problem_info['name'] - problem.description = problem_info['description'] - problem.time_limit = problem_info['time_limit'] - problem.memory_limit = problem_info['memory_limit'] - problem.points = problem_info['points'] - problem.partial = problem_info['partial'] - problem.short_circuit = problem_info['short_circuit'] - problem.group = ProblemGroup.objects.order_by('id').first() # Uncategorized - problem.date = timezone.now() - problem.is_manually_managed = True - with revisions.create_revision(atomic=True): - problem.save() - problem.allowed_languages.set(Language.objects.filter(include_in_problem=True)) - problem.types.set([ProblemType.objects.order_by('id').first()]) # Uncategorized - problem.curators.add(self.request.profile) - revisions.set_user(self.request.user) - revisions.set_comment(_('Imported from %s%s') % ( - settings.VNOJ_PROBLEM_IMPORT_HOST, reverse('problem_detail', args=(form.cleaned_data['problem'],)))) - return HttpResponseRedirect(reverse('problem_edit', args=(problem.code,))) + status = import_problem.delay(user_id=self.request.user.id, **form.cleaned_data) + return redirect_to_task_status( + status, message=_('Importing %s...') % (form.cleaned_data['new_code'],), + redirect=reverse('problem_edit', args=(form.cleaned_data['new_code'],)), + ) def dispatch(self, request, *args, **kwargs): if not settings.VNOJ_PROBLEM_ENABLE_IMPORT: From 4467b1688d194fe07c19a69ca028975e6220913c Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 02:02:57 +0700 Subject: [PATCH 10/22] Disable test data edit on problems with judge_code --- judge/views/problem_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/judge/views/problem_data.py b/judge/views/problem_data.py index f66f4f8dc..216687dd4 100644 --- a/judge/views/problem_data.py +++ b/judge/views/problem_data.py @@ -124,7 +124,7 @@ def _construct_form(self, i, **kwargs): class ProblemManagerMixin(LoginRequiredMixin, ProblemMixin, DetailView): def get_object(self, queryset=None): problem = super(ProblemManagerMixin, self).get_object(queryset) - if problem.is_manually_managed: + if problem.is_manually_managed or problem.judge_code: raise Http404() if self.request.user.is_superuser or problem.is_editable_by(self.request.user): return problem From c77fea00ab3368714608546cd9e684f91af91073 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 02:27:36 +0700 Subject: [PATCH 11/22] Setdefault for storage namespace --- judge/balancer/balancer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/judge/balancer/balancer.py b/judge/balancer/balancer.py index b9826326e..eb0bb0d1e 100644 --- a/judge/balancer/balancer.py +++ b/judge/balancer/balancer.py @@ -64,7 +64,7 @@ def _try_judge(self): self.judge_to_bridge[judge.name] = bridge_id self.bridge_to_judge[bridge_id] = judge - packet['storage-namespace'] = self.config['bridges'][bridge_id].get('storage_namespace') + packet.setdefault('storage-namespace', self.config['bridges'][bridge_id].get('storage_namespace')) judge.submit(packet) def free_judge(self, judge): From 60d042d4ff220f8e20d747ea3f10e573f0ae4eae Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:28:45 +0700 Subject: [PATCH 12/22] Enable CORS regex filter --- dmoj/settings.py | 1 + judge/signals.py | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index 6def7e7dd..695b7eb89 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -29,6 +29,7 @@ ALLOWED_HOSTS = [] CORS_ALLOWED_ORIGINS = [] +CORS_URLS_REGEX = r'^/problem-export/' CSRF_FAILURE_VIEW = 'judge.views.widgets.csrf_failure' diff --git a/judge/signals.py b/judge/signals.py index 738c2aa3c..87c2f8386 100644 --- a/judge/signals.py +++ b/judge/signals.py @@ -2,7 +2,6 @@ import os from typing import Optional -from corsheaders.signals import check_request_enabled from django.conf import settings from django.contrib.flatpages.models import FlatPage from django.core.cache import cache @@ -204,10 +203,3 @@ def registration_user_registered(sender, user, request, **kwargs): with transaction.atomic(): user.save() profile.save() - - -def cors_allow_problem_export_api(sender, request, **kwargs): - return request.path.startswith('/problem-export/') - - -check_request_enabled.connect(cors_allow_problem_export_api) From 1b0acc5921090cde3c27c07229751c273f1b4f37 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:03:05 +0700 Subject: [PATCH 13/22] Fix api server termination procedure --- judge/bridge/monitor.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/judge/bridge/monitor.py b/judge/bridge/monitor.py index cdfd36401..76b720f51 100644 --- a/judge/bridge/monitor.py +++ b/judge/bridge/monitor.py @@ -109,8 +109,9 @@ def __init__(self, judges, **config): class Handler(JudgeControlRequestHandler): signal = self.updater_signal - api_server = HTTPServer(api_listen, Handler) - self.api_server_thread = threading.Thread(target=api_server.serve_forever) + self.api_server = HTTPServer(api_listen, Handler) + else: + self.api_server = None self._handler = SendProblemsHandler(self.updater_signal, self.propagation_signal) self._observer = Observer() @@ -148,7 +149,7 @@ def propagate_update_signal(self): for url in self.update_pings: logger.info('Pinging for problem update: %s', url) try: - with closing(urlopen(url, data='')) as f: + with closing(urlopen(url, data=b'')) as f: f.read() except Exception: logger.exception('Failed to ping for problem update: %s', url) @@ -157,7 +158,6 @@ def updater_thread(self) -> None: while True: self.updater_signal.wait() self.updater_signal.clear() - self.propagation_signal.set() if self.updater_exit: return @@ -172,8 +172,9 @@ def start(self): self.propagator.start() self.updater_signal.set() try: - if hasattr(self, 'api_server_thread'): - self.api_server_thread.start() + if self.api_server: + thread = threading.Thread(target=self.api_server.serve_forever) + thread.start() self._observer.start() except OSError: logger.exception('Failed to start problem monitor.') @@ -181,5 +182,8 @@ def start(self): def stop(self): self._observer.stop() self._observer.join(1) + if self.api_server: + self.api_server.shutdown() self.updater_exit = True self.updater_signal.set() + self.propagation_signal.set() From 1c5152abc3e1a0ef7b811785dc1a84f81a6ba02a Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:10:18 +0700 Subject: [PATCH 14/22] Add delay between iteration --- judge/bridge/monitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/judge/bridge/monitor.py b/judge/bridge/monitor.py index 76b720f51..fa6d365a5 100644 --- a/judge/bridge/monitor.py +++ b/judge/bridge/monitor.py @@ -153,6 +153,7 @@ def propagate_update_signal(self): f.read() except Exception: logger.exception('Failed to ping for problem update: %s', url) + time.sleep(3) def updater_thread(self) -> None: while True: From 69e4c9ed0b73cb35b292e6f4d37aa4f73462bb8d Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:47:34 +0700 Subject: [PATCH 15/22] Enforce non-empty storage_namespace in judge handler --- judge/bridge/judge_handler.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/judge/bridge/judge_handler.py b/judge/bridge/judge_handler.py index 25a739418..6a312cca8 100644 --- a/judge/bridge/judge_handler.py +++ b/judge/bridge/judge_handler.py @@ -237,15 +237,10 @@ def submit(self, id, problem, language, source): data = self.get_related_submission_data(id) self._working = id self._no_response_job = threading.Timer(20, self._kill_if_no_response) - if '/' in problem: - storage_namespace, problem = problem.split('/') - else: - storage_namespace = '' - self.send({ + request = { 'name': 'submission-request', 'submission-id': id, 'problem-id': problem, - 'storage-namespace': storage_namespace, 'language': language, 'source': source if not data.file_only else get_absolute_submission_file_url(source), 'time-limit': data.time, @@ -259,7 +254,14 @@ def submit(self, id, problem, language, source): 'file-only': data.file_only, 'file-size-limit': data.file_size_limit, }, - }) + } + if '/' in problem: + storage_namespace, problem = problem.split('/') + request.update({ + 'storage-namespace': storage_namespace, + 'problem-id': problem, + }) + self.send(request) def _kill_if_no_response(self): logger.error('Judge failed to acknowledge submission: %s: %s', self.name, self._working) From 22a4a99a07e8d33523f6f2e33fa04d4b7e1c3e74 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:17:45 +0700 Subject: [PATCH 16/22] Add key status in import panel --- judge/views/problem_transfer.py | 24 +++++++++++++++++------- templates/problem/import.html | 13 +++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 9c026ea7c..46f6a769f 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -140,22 +140,23 @@ def clean_new_code(self): return new_code def clean(self): - key_info = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() - if not key_info: - self.add_error(None, _('Request timed out')) - elif key_info['remaining_uses'] <= 0: + response = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) + if not response.ok: + self.add_error(None, _('Bad request')) + elif response.json().get('remaining_uses') <= 0: self.add_error('secret', _('No remaining uses')) @shared_task(bind=True) def import_problem(self, user_id, problem, new_code): old_code = problem - problem_info = requests.post(get_problem_export_url(), + response = requests.post(get_problem_export_url(), data={'code': old_code}, - timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT).json() + timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) - if not problem_info: + if not response.ok: raise Http404() + problem_info = response.json() problem = Problem() problem.code = new_code # Use the exported code @@ -200,6 +201,15 @@ def form_valid(self, form): redirect=reverse('problem_edit', args=(form.cleaned_data['new_code'],)), ) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['host_url'] = settings.VNOJ_PROBLEM_IMPORT_HOST + response = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) + context['status'] = response.ok + if response.ok: + context['remaining_uses'] = response.json().get('remaining_uses') + return context + def dispatch(self, request, *args, **kwargs): if not settings.VNOJ_PROBLEM_ENABLE_IMPORT: raise Http404() diff --git a/templates/problem/import.html b/templates/problem/import.html index 2facdee14..39d891d07 100644 --- a/templates/problem/import.html +++ b/templates/problem/import.html @@ -17,6 +17,19 @@ {% endblock %} {% block body %} +
+
+ Host: {{ host_url }} + {% if status %} + + {% else %} + + {% endif %} +
+ {% if status %} +
Remaining uses: {{ remaining_uses }}
+ {% endif %} +
{% if form.non_field_errors() %} From 2ea4dcf492c21d3a5eb3265d4251cf861d84bbb9 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:26:46 +0700 Subject: [PATCH 17/22] Try-except in requests.get --- judge/views/problem_transfer.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 46f6a769f..09b9a4020 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -23,7 +23,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import FormView from django.views.generic.list import BaseListView -from requests.exceptions import HTTPError +from requests.exceptions import HTTPError, RequestException from reversion import revisions from judge.models import Language, Problem, ProblemExportKey, ProblemGroup, ProblemType @@ -204,10 +204,13 @@ def form_valid(self, form): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['host_url'] = settings.VNOJ_PROBLEM_IMPORT_HOST - response = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) - context['status'] = response.ok - if response.ok: - context['remaining_uses'] = response.json().get('remaining_uses') + try: + response = requests.get(get_problem_export_url(), timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) + context['status'] = response.ok + if response.ok: + context['remaining_uses'] = response.json().get('remaining_uses') + except RequestException: + context['status'] = False return context def dispatch(self, request, *args, **kwargs): From eedb3241f8dffd9c53685a77ec42b966b82f0f15 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:29:14 +0700 Subject: [PATCH 18/22] Change alert color --- templates/problem/import.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/problem/import.html b/templates/problem/import.html index 39d891d07..d1d867416 100644 --- a/templates/problem/import.html +++ b/templates/problem/import.html @@ -17,7 +17,7 @@ {% endblock %} {% block body %} -
+
Host: {{ host_url }} {% if status %} From 3fb0d06bdece9a3b464b76f7e3a64219626ef1fc Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:32:05 +0700 Subject: [PATCH 19/22] Disable form on error --- templates/problem/import.html | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/templates/problem/import.html b/templates/problem/import.html index d1d867416..0a1e39204 100644 --- a/templates/problem/import.html +++ b/templates/problem/import.html @@ -30,16 +30,18 @@
Remaining uses: {{ remaining_uses }}
{% endif %}
-
- - {% if form.non_field_errors() %} -
- {{ form.non_field_errors() }} -
- {% endif %} - {% csrf_token %} - {{ form.as_table() }}
-

- -
+ {% if status and remaining_uses > 0 %} +
+
+ {% if form.non_field_errors() %} +
+ {{ form.non_field_errors() }} +
+ {% endif %} + {% csrf_token %} + {{ form.as_table() }}
+

+
+
+ {% endif %} {% endblock %} From 7ab4749b568a09bfa45a8ab8ee47e7d3d6fd7def Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:44:09 +0700 Subject: [PATCH 20/22] Filter out non-empty judge_code on export --- judge/views/problem_transfer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 09b9a4020..96af42156 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -59,7 +59,8 @@ class ProblemExportSelect2View(ProblemExportMixin, BaseListView): def get_queryset(self): if self.transfer_key.remaining_uses <= 0: return Problem.objects.none() - return Problem.get_public_problems().filter(Q(code__icontains=self.term) | Q(name__icontains=self.term)) + return Problem.get_public_problems().filter(Q(judge_code='') & + (Q(code__icontains=self.term) | Q(name__icontains=self.term))) def get(self, request, *args, **kwargs): self.request = request @@ -70,7 +71,7 @@ def get(self, request, *args, **kwargs): return JsonResponse({ 'results': [ { - 'text': obj.name, + 'text': f'{obj.name} ({obj.code})', 'id': obj.code, } for obj in context['object_list']], 'more': context['page_obj'].has_next(), From 10f8fe22fa3d2901a7f4faf34a80278014c171da Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:37:24 +0700 Subject: [PATCH 21/22] Fix lint --- judge/views/problem_transfer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 96af42156..966620a04 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -152,8 +152,8 @@ def clean(self): def import_problem(self, user_id, problem, new_code): old_code = problem response = requests.post(get_problem_export_url(), - data={'code': old_code}, - timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) + data={'code': old_code}, + timeout=settings.VNOJ_PROBLEM_IMPORT_TIMEOUT) if not response.ok: raise Http404() From b586a547be5a8613188f8dc67035a6c3e1304b42 Mon Sep 17 00:00:00 2001 From: Nguyen Viet Dung <29406816+magnified103@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:09:41 +0700 Subject: [PATCH 22/22] Monitor URL defaults to None --- dmoj/settings.py | 2 +- judge/views/problem_transfer.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index 695b7eb89..79ddbda78 100755 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -682,7 +682,7 @@ BRIDGED_JUDGE_PROXIES = None BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)] BRIDGED_DJANGO_CONNECT = None -BRIDGED_MONITOR_UPDATE_URL = 'http://localhost:9990/update/problems' +BRIDGED_MONITOR_UPDATE_URL = None # Event Server configuration EVENT_DAEMON_USE = False diff --git a/judge/views/problem_transfer.py b/judge/views/problem_transfer.py index 966620a04..3ca398633 100644 --- a/judge/views/problem_transfer.py +++ b/judge/views/problem_transfer.py @@ -182,12 +182,13 @@ def import_problem(self, user_id, problem, new_code): revisions.set_comment(_('Imported from %s%s') % ( settings.VNOJ_PROBLEM_IMPORT_HOST, reverse('problem_detail', args=(old_code,)))) url = settings.BRIDGED_MONITOR_UPDATE_URL - logger.info('Pinging for problem update: %s', url) - try: - with closing(urlopen(url, data=b'')) as f: - f.read() - except Exception: - logger.exception('Failed to ping for problem update: %s', url) + if url: + logger.info('Pinging for problem update: %s', url) + try: + with closing(urlopen(url, data=b'')) as f: + f.read() + except Exception: + logger.exception('Failed to ping for problem update: %s', url) class ProblemImportView(TitleMixin, FormView):