diff --git a/db/db.ddl b/db/db.ddl index 7e27eb955a61e9ab922a18fdf60801f9699f9e5b..fd3ce6c2c8f86c9b736993a36e2911491ddd23f6 100644 --- a/db/db.ddl +++ b/db/db.ddl @@ -107,6 +107,7 @@ CREATE TABLE rounds ( score_mode score_mode NOT NULL DEFAULT 'basic', -- mód výsledkovky score_winner_limit int DEFAULT NULL, -- bodový limit na označení za vítěze score_successful_limit int DEFAULT NULL, -- bodový limit na označení za úspěšného řešitele + points_step numeric(2,1) NOT NULL DEFAULT 1, -- s jakou přesností jsou přidělovány body (celé aneb 1, 0.5, 0.1) has_messages boolean NOT NULL DEFAULT false, -- má zprávičky UNIQUE (year, category, seq, part) ); @@ -162,7 +163,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ů + max_points numeric(5,1) DEFAULT NULL, -- maximální počet bodů, pokud je nastaven, nelze zadat více bodů UNIQUE (round_id, code) ); @@ -200,7 +201,7 @@ CREATE TABLE solutions ( user_id int NOT NULL REFERENCES users(user_id), final_submit int DEFAULT NULL REFERENCES papers(paper_id), -- verze odevzdání, která se má hodnotit final_feedback int DEFAULT NULL REFERENCES papers(paper_id), -- verze komentáře opravovatelů, kterou má vidět účastník - points int DEFAULT NULL, + points numeric(5,1) DEFAULT NULL, note text NOT NULL DEFAULT '', -- komentář pro řešitele org_note text NOT NULL DEFAULT '', -- komentář viditelný jen organizátorům PRIMARY KEY (task_id, user_id) @@ -213,7 +214,7 @@ CREATE TABLE points_history ( points_history_id serial PRIMARY KEY, task_id int NOT NULL REFERENCES tasks(task_id), participant_id int NOT NULL REFERENCES users(user_id), - points int DEFAULT NULL, + points numeric(5,1) DEFAULT NULL, points_by int NOT NULL REFERENCES users(user_id), -- kdo přidělil body points_at timestamp with time zone NOT NULL -- a kdy ); diff --git a/db/upgrade-20210328.sql b/db/upgrade-20210328.sql new file mode 100644 index 0000000000000000000000000000000000000000..f237e0c816f96b9c2d0ea05d8cdf49acdb715040 --- /dev/null +++ b/db/upgrade-20210328.sql @@ -0,0 +1,13 @@ +SET ROLE 'mo_osmo'; + +ALTER TABLE rounds + ADD COLUMN points_step numeric(2,1) NOT NULL DEFAULT 1; -- s jakou přesností jsou přidělovány body (celé aneb 1, 0.5, 0.1) + +ALTER TABLE solutions + ALTER COLUMN points SET DATA TYPE numeric(5,1); + +ALTER TABLE points_history + ALTER COLUMN points SET DATA TYPE numeric(5,1); + +ALTER TABLE tasks + ALTER COLUMN max_points SET DATA TYPE numeric(5,1); diff --git a/mo/db.py b/mo/db.py index 5d2c50189a63c27e52d5a01d9a96d216c8eb8e14..81a3c0b89862d05575e4d14607acb8a48199a06d 100644 --- a/mo/db.py +++ b/mo/db.py @@ -15,6 +15,7 @@ from sqlalchemy.orm.attributes import get_history from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.sql.expression import CTE +from sqlalchemy.sql.sqltypes import Numeric from typing import Optional, List, Tuple import mo @@ -186,6 +187,15 @@ round_score_mode_names = { } +# V DB jako numeric(2,1), používá se tak snadněji, než enum +round_points_step_names = { + 1: "Celé body", + 0.5: "Půlbody", + 0.1: "Desetinné body", +} +round_points_step_choices = round_points_step_names.items() + + class Round(Base): __tablename__ = 'rounds' __table_args__ = ( @@ -209,6 +219,7 @@ class Round(Base): score_mode = Column(Enum(RoundScoreMode, name='score_mode'), nullable=False, server_default=text("'basic'::score_mode")) score_winner_limit = Column(Integer) score_successful_limit = Column(Integer) + points_step = Column(Numeric, nullable=False) has_messages = Column(Boolean, nullable=False, server_default=text("false")) master = relationship('Round', primaryjoin='Round.master_round_id == Round.round_id', remote_side='Round.round_id', post_update=True) @@ -246,6 +257,11 @@ class Round(Base): else: return self.state + def points_step_name(self) -> str: + if float(self.points_step) in round_points_step_names: + return round_points_step_names[float(self.points_step)] + return str(self.points_step) + class User(Base): __tablename__ = 'users' @@ -415,7 +431,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) + max_points = Column(Numeric) round = relationship('Round') @@ -524,7 +540,7 @@ class PointsHistory(Base): points_history_id = Column(Integer, primary_key=True, server_default=text("nextval('points_history_points_history_id_seq'::regclass)")) task_id = Column(Integer, ForeignKey('tasks.task_id'), nullable=False) participant_id = Column(Integer, ForeignKey('users.user_id'), nullable=False) - points = Column(Integer) + points = Column(Numeric) points_by = Column(Integer, ForeignKey('users.user_id'), nullable=False) points_at = Column(DateTime(True), nullable=False) @@ -540,7 +556,7 @@ class Solution(Base): user_id = Column(Integer, ForeignKey('users.user_id'), primary_key=True, nullable=False) final_submit = Column(Integer, ForeignKey('papers.paper_id')) final_feedback = Column(Integer, ForeignKey('papers.paper_id')) - points = Column(Integer) + points = Column(Numeric) note = Column(Text, nullable=False, server_default=text("''::text")) org_note = Column(Text, nullable=False, server_default=text("''::text")) diff --git a/mo/score.py b/mo/score.py index ccbefbf0909b9c72a216bc2855a1ce85a3cd42fb..01a26d786d8157cb36364e19f69d24ace9abf42c 100644 --- a/mo/score.py +++ b/mo/score.py @@ -1,3 +1,4 @@ +import decimal from fractions import Fraction from sqlalchemy import and_ from sqlalchemy.orm import joinedload @@ -56,8 +57,8 @@ class ScoreResult: def get_sols_map(self) -> Dict[int, db.Solution]: return self._sols[0] - def get_total_points(self) -> int: - sum = 0 + def get_total_points(self) -> decimal.Decimal: + sum = decimal.Decimal(0) for sol in self.get_sols(): if sol.points: sum += sol.points @@ -67,17 +68,17 @@ class ScoreResult: class ScoreTask: task: db.Task num_solutions: int - sum_points: int + sum_points: decimal.Decimal def __init__(self, task: db.Task): self.task = task self.num_solutions = 0 - self.sum_points = 0 + self.sum_points = decimal.Decimal(0) def get_difficulty(self) -> Fraction: if self.num_solutions == 0: return Fraction(0) - return Fraction(self.sum_points, self.num_solutions) + return Fraction(Fraction(self.sum_points), self.num_solutions) def get_difficulty_str(self) -> str: return f'{self.sum_points}/{self.num_solutions}' diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 96b94c61c878b66ee0b828bd20545aaa07261e87..70727966cb2d2dcb14936475de2f84c46a3f3587 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -431,6 +431,10 @@ class RoundEditForm(FlaskForm): "Hranice bodů pro úspěšné řešitele", validators=[validators.Optional()], description="Řešitelé s alespoň tolika body budou označeni za úspěšné řešitele, prázdná hodnota = žádné neoznačovat", ) + points_step = wtforms.SelectField( + "Přesnost bodování", choices=db.round_points_step_choices, + description="Ovlivňuje možnost zadávání nových bodů, již uložené body nezmění" + ) has_messages = wtforms.BooleanField("Zprávičky pro účastníky (aktivuje možnost vytvářet novinky zobrazované účastníkům)") submit = wtforms.SubmitField('Uložit') @@ -450,6 +454,7 @@ def org_round_edit(id: int): del form.score_mode del form.score_winner_limit del form.score_successful_limit + del form.points_step if form.validate_on_submit(): form.populate_obj(round) diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html index aa1a62a677497f02ed0459b0d5b7c2edb15e8b05..ac93210581020b547d1cd0edba25805f79b6876f 100644 --- a/mo/web/templates/org_round.html +++ b/mo/web/templates/org_round.html @@ -61,6 +61,7 @@ <tr><td>Výsledková listina<td>{{ round.master.score_mode.friendly_name() }} <tr><td>Hranice bodů pro vítěze<td>{{ round.master.score_winner_limit|none_value(Markup('<i>nenastaveno</i>')) }} <tr><td>Hranice bodů pro úspěšné řešitele<td>{{ round.master.score_successful_limit|none_value(Markup('<i>nenastaveno</i>')) }} + <tr><td>Přesnost bodování<td>{{ round.points_step_name() }} </table> <div style="clear: both;"></div>