diff --git a/db/db.ddl b/db/db.ddl index ae71eae76c401b591c1141c55d4aa1514e736f58..b5e22cfc96b62abf2fe8c74c33b42e9389beb2fe 100644 --- a/db/db.ddl +++ b/db/db.ddl @@ -152,6 +152,7 @@ CREATE TABLE tasks ( round_id int NOT NULL REFERENCES rounds(round_id), code varchar(255) NOT NULL, -- např. "P-I-1" name varchar(255) NOT NULL, + max_points int DEFAULT NULL, -- maximální počet bodů, pokud je nastaven, nelze zadat více bodů UNIQUE (round_id, code) ); diff --git a/db/upgrade-20210307.sql b/db/upgrade-20210307.sql new file mode 100644 index 0000000000000000000000000000000000000000..bc7076efeae71b2b355a0929cab27fad52241b42 --- /dev/null +++ b/db/upgrade-20210307.sql @@ -0,0 +1,4 @@ +SET ROLE 'mo_osmo'; + +ALTER TABLE tasks + ADD COLUMN max_points int DEFAULT NULL; -- maximální počet bodů, pokud je nastaven, nelze zadat více bodů diff --git a/mo/db.py b/mo/db.py index f24e55c8263bf239562b85216e7a8c2a6c36e8da..7ddf905cb220d7ec712e78c8f4c25310fd030920 100644 --- a/mo/db.py +++ b/mo/db.py @@ -364,6 +364,7 @@ class Task(Base): round_id = Column(Integer, ForeignKey('rounds.round_id'), nullable=False) code = Column(String(255), nullable=False) name = Column(String(255), nullable=False) + max_points = Column(Integer) round = relationship('Round') diff --git a/mo/imports.py b/mo/imports.py index 72c7b7b3dc98fdff4878118727dd51225f15254a..6d92c8e7082c37b9edbedb5e2e3ce89f1eaeb0ae 100644 --- a/mo/imports.py +++ b/mo/imports.py @@ -228,6 +228,10 @@ class Import: if pts < 0: return self.error('Body nesmí být záporné') + assert self.task is not None + if self.task.max_points is not None and pts > self.task.max_points: + return self.error(f'Body převyšují maximální počet bodů pro tuto úlohu ({self.task.max_points})') + return pts def find_or_create_participant(self, user: db.User, year: int, school_id: int, birth_year: int, grade: str) -> Optional[db.Participant]: diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 468f29ef34100b722976056f144e6d2fded70c93..933d9d2ae77dfadc2b096c04f965988bd67279fb 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -26,6 +26,7 @@ from mo.web.util import PagerForm from mo.web.table import CellCheckbox, Table, Row, Column, cell_pion_link, cell_place_link, cell_email_link import wtforms.validators as validators from wtforms.fields.html5 import IntegerField +from wtforms.widgets.html5 import NumberInput class ImportForm(FlaskForm): @@ -637,7 +638,8 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio class SubmitForm(FlaskForm): note = wtforms.TextAreaField("Poznámka pro účastníka", description="Viditelná účastníkovi po uzavření kola") org_note = wtforms.TextAreaField("Interní poznámka", description="Viditelná jen organizátorům") - points = IntegerField('Body', description="Účastník po uzavření kola uvidí jen naposledy zadané body", render_kw={'min': 0}, validators=[validators.Optional()]) + # Validátory k points budou přidány podle počtu maximálních bodů úlohy v org_submit_list + points = IntegerField('Body', description="Účastník po uzavření kola uvidí jen naposledy zadané body") submit = wtforms.SubmitField('Uložit') file = flask_wtf.file.FileField("Soubor") @@ -715,6 +717,11 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option return redirect(self_url) form = SubmitForm(obj=sol) + form.points.validators = [ + validators.Optional(), + validators.NumberRange(min=0, max=sc.task.max_points, message="Počet bodů musí být mezi %(min)s a %(max)s") + ] + form.points.widget = NumberInput(min=0, max=sc.task.max_points) # min a max v HTML if form.validate_on_submit(): if sol and form.delete.data: if sol.final_submit or sol.final_feedback: @@ -736,9 +743,6 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option points = form.points.data # Checks - if sol and sc.allow_edit_points and points and points < 0: - flash('Nelze zadat záporné body, žádné změny nebyly uloženy', 'danger') - return redirect(self_url) if (form.submit_sol.data or form.submit_fb.data) and form.file.data is None: flash('Schází soubor k nahrání, žádné změny nebyly uloženy', 'danger') return redirect(self_url) @@ -982,6 +986,10 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non flash('Nelze zadat záporné body', 'danger') ok = False break + elif points and sc.task.max_points is not None and points > sc.task.max_points: + flash(f'Maximální počet bodů za úlohu je {sc.task.max_points}, nelze zadat více', 'danger') + ok = False + break if points != sol.points: # Save points diff --git a/mo/web/org_round.py b/mo/web/org_round.py index cb4a7a75160266c9181273ebb2dccaea25dc819c..9235d248ed68d91fa94dc4be8b668b58899486b6 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -205,6 +205,10 @@ class TaskEditForm(FlaskForm): validators.Regexp(r'^([^\W_]|-)+$', message="Kód úlohy smí obsahovat jen písmena, čísla a znak -"), ]) name = wtforms.StringField('Název úlohy') + max_points = IntegerField( + 'Maximum bodů', validators=[validators.Optional()], + description="Při nastavení maxima nelze udělit více bodů, pro zrušení uložte prázdnou hodnotu", + ) submit = wtforms.SubmitField('Uložit') diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index 510d74c2ed9320cc8f008fdef6d69a0a8bfb27ee..afed4ec18db6334194cd06b8f18d07e7de922a64 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -84,6 +84,7 @@ <th>Kód <th>Název <th>Odevzdaná řešení + <th>Maximum bodů <th>Jednotlivé akce <th>Dávkové operace </tr> @@ -93,6 +94,7 @@ <td>{{ task.code }} <td>{{ task.name }} <td>{{ task.sol_count }} + <td>{{ task.max_points|none_value('–') }} <td><div class="btn-group"> <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_task', contest_id=contest.contest_id, task_id=task.task_id, site_id=site_id) }}">Odevzdaná řešení</a> {% if not site and can_edit_points %} diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html index f7b53848f33b9c106bb9d3876358288c51afb2b0..f8109b80a85e7ed910240225a6f5ee5de95d8820 100644 --- a/mo/web/templates/org_round.html +++ b/mo/web/templates/org_round.html @@ -92,6 +92,7 @@ <th>Kód <th>Název <th>Odevzdaná řešení + <th>Maximum bodů {% if can_manage_round %}<th>Akce{% endif %} {% if can_handle_submits or can_upload %}<th>Dávkové operace{% endif %} </tr> @@ -101,6 +102,7 @@ <td>{{ task.code }} <td>{{ task.name }} <td>{{ task.sol_count }} + <td>{{ task.max_points|none_value('–') }} {% if can_manage_round %} <td><div class="btn-group"> <a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_edit', id=round.round_id, task_id=task.task_id) }}">Editovat</a> diff --git a/mo/web/templates/org_submit_list.html b/mo/web/templates/org_submit_list.html index 63c9051133f7b3fe15ce6c9eae04241ddef24790..9c934ce2dd474665b713c53b1c08b71b5545359c 100644 --- a/mo/web/templates/org_submit_list.html +++ b/mo/web/templates/org_submit_list.html @@ -12,7 +12,9 @@ <tr><th>Účastník<td>{{ sc.user|pion_link(sc.contest.contest_id) }} <tr><th>Úloha<td><a href='{{ url_for('org_contest_task', contest_id=sc.contest.contest_id, site_id=site_id, task_id=sc.task.task_id) }}'>{{ sc.task.code }} {{ sc.task.name }}</a> {% if solution %} - <tr><th>Body<td>{% if solution.points is not none %}{{solution.points}}{% else %}<span class="unknown">?</span>{% endif %} + <tr><th>Body<td> + {% if solution.points is not none %}{{solution.points}}{% else %}<span class="unknown">?</span>{% endif %} + {% if sc.task.max_points is not none %}<span class="hint"> / {{ sc.task.max_points }}</span>{% endif %} <tr title="Viditelná účastníkovi po uzavření kola"> <th>Poznámka k řešení:<td style="white-space: pre;">{{ solution.note|or_dash }}</td> <tr title="Viditelná jen organizátorům"> diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index 150d77ecb0f50d4af978c31b7e65e4557fa9c949..a9340f07d23895a0acdda8be4bfe5ef4d86cf693 100644 --- a/mo/web/templates/parts/org_solution_table.html +++ b/mo/web/templates/parts/org_solution_table.html @@ -85,7 +85,7 @@ finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solut {% if sol.org_note %} <span class="icon" title="Interní poznámka: {{ sol.org_note }}">🗩</span>{% endif %} <td> {% if points_form %} - <input type="number" class="form-control" name="points_{{u.user_id}}" value="{{ request_form.get("points_{}".format(u.user_id)) or sol.points }}" size="4"> + <input type="number" min=0 {% if task.max_points is not none %}max={{ task.max_points }}{% endif %} class="form-control" name="points_{{u.user_id}}" value="{{ request_form.get("points_{}".format(u.user_id)) or sol.points }}" size="4"> {% else %} {% if sol.points is not none %}{{ sol.points}}{% else %}<span class="unknown">?</span>{% endif %} {% endif %} diff --git a/static/mo.css b/static/mo.css index c90f0243777b1901eae4212b67a6ceb0045ee9b9..cf2b55f0a3a7b17dd959c3dd00da0203266eef15 100644 --- a/static/mo.css +++ b/static/mo.css @@ -61,6 +61,10 @@ footer { font-weight: bold; } +.hint { + color: #737373; +} + span.unknown { font-weight: bold; color: red;