Skip to content

Editorials #525

New issue

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

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

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e2d7421
Add ProblemEditorial class like ProblemStatement class.
segir187 May 28, 2025
9266f73
Add initial, stupid editorial tab implementation.
segir187 May 28, 2025
af79e58
Abstract away query_statement contents into query_document and call i…
segir187 May 28, 2025
1d737b9
Move editorial tab closer to statement.
segir187 May 28, 2025
b5dd303
Partially-correct abstraction of problem_site_statement -> problem_si…
segir187 May 28, 2025
5e143a2
Better check_for_statement implementation.
segir187 May 28, 2025
b46875b
Add no-problem-editorial.html template.
segir187 May 28, 2025
b43e247
Load different page depending on document type.
segir187 May 28, 2025
e5a0afe
Generalise problem_site_statement_zip_view into problem_site_document…
segir187 May 28, 2025
9f0f531
statement -> document in query_zip.
segir187 May 28, 2025
24932ea
Rename from-zip-statemnt.html template to from-zip-document.html.
segir187 May 28, 2025
94ed2a0
Pass type argument to problem_site_document_zip_view in problem_site_…
segir187 May 28, 2025
bedd34c
Generalise problem_site_external_statement_view -> problem_site_exter…
segir187 May 28, 2025
520f687
Make problem_site_document_zip_view throw Http404 for unknown documen…
segir187 May 28, 2025
010e8fe
Correct document_url setting for external-statement.html page.
segir187 May 28, 2025
840d0dc
Rename external-statement.html -> external-document.html.
segir187 May 28, 2025
816fe54
Generalise external-document.html contents.
segir187 May 28, 2025
554d851
Wording change.
segir187 May 28, 2025
a623664
Consistent case ordering in problem_site_document.
segir187 May 28, 2025
6fda6e8
Problem statement views before editorial views.
segir187 May 28, 2025
1309e1d
Fix view for pattern problem_site_statement_zip.
segir187 May 28, 2025
25345cf
Merge branch 'sio2project:master' into Editorials
segir187 Jun 4, 2025
a2afaf4
Rename replace_problem_statement tab to replace_statement_or_editorial.
segir187 Jun 5, 2025
870d878
Update problem_site_replace_statement -> problem_site_replace_stateme…
segir187 Jun 10, 2025
ccc5ef6
replace-problem-statement.html with two identical forms, one for stat…
segir187 Jun 10, 2025
b19cf67
Restore replace-problem-statement.html to previous form.
segir187 Jun 10, 2025
2b0892e
Initial implementation of two forms via includes.
segir187 Jun 10, 2025
bd536c6
Merge branch 'sio2project:master' into Editorials
segir187 Jun 10, 2025
eb3fa8e
Migration to create ProblemEditorial model.
segir187 Jun 10, 2025
33410d3
Slightly better includes (still not working).
segir187 Jun 10, 2025
065130f
Restore original version of replace-problem-statement.html.
segir187 Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion oioioi/contests/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def _replace_statement_href(self, instance):
return (
reverse('problem_site', args=(instance.problem.problemsite.url_key,))
+ '?'
+ urllib.parse.urlencode({'key': 'replace_problem_statement'})
+ urllib.parse.urlencode({'key': 'replace_statement_or_editorial'})
)

def _package_manage_href(self, instance):
Expand Down
29 changes: 29 additions & 0 deletions oioioi/problems/migrations/0039_problemeditorial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.2.3 on 2025-06-10 20:59

import django.db.models.deletion
import oioioi.filetracker.fields
import oioioi.problems.models
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('problems', '0038_alter_algorithmtaglocalization_language_and_more'),
]

operations = [
migrations.CreateModel(
name='ProblemEditorial',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language', models.CharField(blank=True, max_length=6, null=True, verbose_name='language code')),
('content', oioioi.filetracker.fields.FileField(max_length=255, upload_to=oioioi.problems.models.make_problem_filename, verbose_name='content')),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='editorials', to='problems.problem')),
],
options={
'verbose_name': 'problem editorial',
'verbose_name_plural': 'problem editorials',
},
),
]
34 changes: 34 additions & 0 deletions oioioi/problems/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,40 @@ def __str__(self):
return '%s / %s' % (self.problem.name, self.filename)


class ProblemEditorial(models.Model):
"""Represents a file containing problem editoral.

Problem may have multiple editorials, for example in various languages
or formats.
"""

problem = models.ForeignKey(
Problem, related_name='editorials', on_delete=models.CASCADE
)
language = models.CharField(
max_length=6, blank=True, null=True, verbose_name=_("language code")
)
content = FileField(upload_to=make_problem_filename, verbose_name=_("content"))

@property
def filename(self):
return os.path.split(self.content.name)[1]

@property
def download_name(self):
return self.problem.short_name + self.extension

@property
def extension(self):
return os.path.splitext(self.content.name)[1].lower()

class Meta(object):
verbose_name = _("problem editorial")
verbose_name_plural = _("problem editorials")

def __str__(self):
return '%s / %s' % (self.problem.name, self.filename)


class ProblemAttachment(models.Model):
"""Represents an additional file visible to the contestant, linked to
Expand Down
163 changes: 108 additions & 55 deletions oioioi/problems/problem_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
ProblemAttachment,
ProblemPackage,
ProblemStatement,
ProblemEditorial,
)
from oioioi.problems.problem_sources import UploadedPackageSource
from oioioi.problems.utils import (
can_modify_tags,
can_admin_problem,
generate_add_to_contest_metadata,
generate_model_solutions_context,
query_editorial,
query_statement,
query_zip,
)
Expand Down Expand Up @@ -79,53 +81,91 @@ def decorator(func):
return decorator


def problem_site_statement_zip_view(request, site_key, path):
def problem_site_document_zip_view(request, site_key, path, type='statement'):
problem = get_object_or_404(Problem, problemsite__url_key=site_key)
statement = query_statement(problem.id)
if not statement:
raise Http404
return query_zip(statement, path)

document = None
if type == 'editorial':
document = query_editorial(problem.id)
elif type == 'statement':
document = query_statement(problem.id)

def check_for_statement(request, problem):
"""Function checking if given problem has a ProblemStatement."""
return bool(ProblemStatement.objects.filter(problem=problem))
if not document:
raise Http404
return query_zip(document, path)


@problem_site_tab(
_("Problem statement"), key='statement', order=100, condition=check_for_statement
)
def problem_site_statement(request, problem):
statement = query_statement(problem.id)
if not statement:
statement_html = render_to_string(
'problems/no-problem-statement.html',
{'problem': problem,
'can_admin_problem': can_admin_problem(request, problem)}
)
elif statement.extension == '.zip':
response = problem_site_statement_zip_view(
request, problem.problemsite.url_key, 'index.html'
def problem_site_document(request, problem, document, type):
if not document:
if type == 'statement':
document_html = render_to_string(
'problems/no-problem-statement.html',
{'problem': problem,
'can_admin_problem': can_admin_problem(request, problem)}
)
elif type == 'editorial':
document_html = render_to_string(
'problems/no-problem-editorial.html',
{'problem': problem,
'can_admin_problem': can_admin_problem(request, problem)}
)
else:
raise Http404("Document not found")
elif document.extension == '.zip':
response = problem_site_document_zip_view(
request, problem.problemsite.url_key, 'index.html', type
)
statement_html = render_to_string(
'problems/from-zip-statement.html',
document_html = render_to_string(
'problems/from-zip-document.html',
{'problem': problem,
'statement': mark_safe(response.content.decode(errors="replace")),
'can_admin_problem': can_admin_problem(request, problem)}
)
else:
statement_url = reverse(
'problem_site_external_statement',
kwargs={'site_key': problem.problemsite.url_key},
)
statement_html = render_to_string(
'problems/external-statement.html',
document_url = None
if type == 'statement':
document_url = reverse(
'problem_site_external_statement',
kwargs={'site_key': problem.problemsite.url_key},
)
elif type == 'editorial':
document_url = reverse(
'problem_site_external_editorial',
kwargs={'site_key': problem.problemsite.url_key},
)
else:
raise Http404("Document not found")

document_html = render_to_string(
'problems/external-document.html',
{'problem': problem,
'statement_url': statement_url,
'document_url': document_url,
'document_type': type,
'can_admin_problem': can_admin_problem(request, problem)},
)

return statement_html
return document_html

def check_for_statement(request, problem):
return ProblemStatement.objects.filter(problem=problem).exists()

@problem_site_tab(
_("Problem statement"), key='statement', order=100, condition=check_for_statement
)
def problem_site_statement(request, problem):
statement = query_statement(problem.id)
return problem_site_document(request, problem, statement, type='statement')


def show_editorial(request, problem):
return ProblemEditorial.objects.filter(problem=problem).exists() and not request.contest

@problem_site_tab(
_("Editorial"), key='editorial', order=750, condition=show_editorial
)
def problem_site_editorial(request, problem):
statement = query_editorial(problem.id)
return problem_site_document(request, problem, statement, type='editorial')


def check_for_downloads(request, problem):
Expand Down Expand Up @@ -309,37 +349,50 @@ def problem_site_add_to_contest(request, problem):


@problem_site_tab(
_("Replace problem statement"),
key='replace_problem_statement',
_("Replace statement or editorial"),
key='replace_statement_or_editorial',
order=800,
condition=can_admin_problem,
)
def problem_site_replace_statement(request, problem):
statements = ProblemStatement.objects.filter(problem=problem)
filenames = [statement.filename for statement in statements]
def problem_site_replace_statement_or_editorial(request, problem):
statements = ProblemStatement .objects.filter(problem=problem)
editorials = ProblemEditorial.objects.filter(problem=problem)

stmt_names = [s.filename for s in statements]
ed_names = [e.filename for e in editorials]

stmt_form = ProblemStatementReplaceForm(stmt_names)
ed_form = ProblemStatementReplaceForm(ed_names)

if request.method == 'POST':
form = PackageFileReuploadForm(filenames, request.POST, request.FILES)
if form.is_valid():
statement_filename = form.cleaned_data['file_name']
statements = [s for s in statements if s.filename == statement_filename]
if statements:
statement = statements[0]
new_statement_file = form.cleaned_data['file_replacement']
statement.content = new_statement_file
statement.save()
url = reverse(
'problem_site', kwargs={'site_key': problem.problemsite.url_key}
)
return redirect(url + '?key=replace_problem_statement')
else:
form.add_error(None, _("Picked statement file does not exist."))
else:
form = ProblemStatementReplaceForm(filenames)
form_type = request.POST.get('form_type')
if form_type == 'statement':
stmt_form = ProblemStatementReplaceForm(stmt_names, request.POST, request.FILES)
if stmt_form.is_valid():
fn = stmt_form.cleaned_data['file_name']
stmt = next(s for s in statements if s.filename == fn)
stmt.content = stmt_form.cleaned_data['file_replacement']
stmt.save()
url = reverse('problem_site', kwargs={'site_key': problem.problemsite.url_key})
return redirect(url + '?key=replace_statement_or_editorial')
elif form_type == 'editorial':
ed_form = ProblemStatementReplaceForm(ed_names, request.POST, request.FILES)
if ed_form.is_valid():
fn = ed_form.cleaned_data['file_name']
ed = next(e for e in editorials if e.filename == fn)
ed.content = ed_form.cleaned_data['file_replacement']
ed.save()
url = reverse('problem_site', kwargs={'site_key': problem.problemsite.url_key})
return redirect(url + '?key=replace_statement_or_editorial')

return TemplateResponse(
request,
'problems/replace-problem-statement.html',
{'form': form, 'problem': problem},
{
'problem': problem,
'form': stmt_form,
'editorial_form': ed_form,
},
)


Expand Down
22 changes: 22 additions & 0 deletions oioioi/problems/templates/problems/external-document.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% load i18n %}
<div>
<object data="{{ document_url }}" type="application/pdf" width="100%" height="800px"></object>
</div>

<div class="text-center bottom-text">
{% if document_type == 'statement' %}
{% blocktrans %}You can also open the problem's statement by clicking <a href="{{ document_url }}"><b>here</b></a>.{% endblocktrans %}
{% elif document_type == 'editorial' %}
{% blocktrans %}You can also open the problem's editorial by clicking <a href="{{ document_url }}"><b>here</b></a>.{% endblocktrans %}
{% else %}
{% blocktrans %}You can also open this document by clicking <a href="{{ document_url }}"><b>here</b></a>.{% endblocktrans %}
{% endif %}
</div>

{% if can_admin_problem %}
<div class="text-center bottom-text">
{% blocktrans %}<p>Problem's author: </p>{% endblocktrans %}
<p>{{ problem.author.first_name }} {{problem.author.last_name}} ({{problem.author.username}})</p>
<p><a href="mailto:{{problem.author.email}}">Send e-mail to the author</a></p>
</div>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<h4>{{ title }}</h4>
<form enctype="multipart/form-data" method="post">

{% csrf_token %}
{% if form.non_field_errors %}
<div class="form-group">
{% for error in form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<span class="sr-only">{% trans "Error" %}:</span>
{{ error }}
</div>
{% endfor %}
</div>
{% endif %}

<div class="form-group {% if form.file_name.errors %}has-error{% endif %}">
{% if display_labels != False %}
<label for="{{ form.file_name.auto_id }}" class="control-label">
{{ form.file_name.label }}
</label>
{% endif %}
{{ form.file_name | add_class:"form-control" }}
{% for error in form.file_name.errors %}
<div class="{% if inline %}help-inline{% else %}help-block{% endif %}">{{ error }}</div>
{% endfor %}
{% if form.file_name.help_text %}
<div class="{% if inline %}help-inline{% else %}help-block{% endif %}">{{ form.file_name.help_text }}</div>
{% endif %}
</div>

<div class="form-group {% if form.file_replacement.errors %}has-error{% endif %}">
{% if display_labels != False %}
<label for="{{ form.file_replacement.auto_id }}" class="control-label">
{{ form.file_replacement.label }}
</label>
{% endif %}
{{ form.file_replacement | add_class:"form-control" }}
{% for error in form.file_replacement.errors %}
<div class="{% if inline %}help-inline{% else %}help-block{% endif %}">{{ error }}</div>
{% endfor %}
{% if form.file_replacement.help_text %}
<div class="{% if inline %}help-inline{% else %}help-block{% endif %}">{{ form.file_replacement.help_text }}</div>
{% endif %}
</div>

<div class="form-group">
{% block upload_btn %}
<button type="submit" name="upload_button" class="btn btn-primary">{% trans "Submit" %}</button>
{% endblock %}
</div>
</form>
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{% load i18n %}
<div>
<object data="{{ statement_url }}" type="application/pdf" width="100%" height="800px"></object>
</div>

<div class="text-center bottom-text">
{% blocktrans %}You can also open the problem's statement by clicking <a href="{{ statement_url }}"><b>here</b></a>.{% endblocktrans %}
<div class="alert alert-info">
{% blocktrans %}
<p>This problem doesn't have any editorial available .</p>
{% endblocktrans %}
</div>

{% if can_admin_problem %}
Expand Down
2 changes: 1 addition & 1 deletion oioioi/problems/tests/test_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ def test_tags_tab_user_with_permission(self):
def test_statement_replacement(self):
url = (
reverse('problem_site', kwargs={'site_key': '123'})
+ '?key=replace_problem_statement'
+ '?key=replace_statement_or_editorial'
)

self.assertTrue(self.client.login(username='test_user'))
Expand Down
Loading
Loading