diff --git a/db/db.ddl b/db/db.ddl index b5e22cfc96b62abf2fe8c74c33b42e9389beb2fe..803d275d87724c5db2362f5948248bb399277ad8 100644 --- a/db/db.ddl +++ b/db/db.ddl @@ -90,9 +90,11 @@ CREATE TYPE score_mode AS ENUM ( CREATE TABLE rounds ( round_id serial PRIMARY KEY, + master_round_id int DEFAULT NULL REFERENCES rounds(round_id), year int NOT NULL, -- ročník MO category varchar(2) NOT NULL, -- "A", "Z5" apod. seq int NOT NULL, -- 1=domácí kolo atd. + part int NOT NULL DEFAULT 0, -- část kola (u dělených kol) level int NOT NULL, -- úroveň hierarchie míst name varchar(255) NOT NULL, -- zobrazované jméno ("Krajské kolo" apod.) state round_state NOT NULL DEFAULT 'preparing', -- stav kola @@ -104,17 +106,22 @@ 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 - UNIQUE (year, category, seq) + UNIQUE (year, category, seq, part) ); +CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id); + -- Soutěže (instance kola v konkrétním místě) CREATE TABLE contests ( - contest_id serial PRIMARY KEY, - round_id int NOT NULL REFERENCES rounds(round_id), - place_id int NOT NULL REFERENCES places(place_id), + contest_id serial PRIMARY KEY, + master_contest_id int DEFAULT NULL REFERENCES contests(contest_id), + round_id int NOT NULL REFERENCES rounds(round_id), + place_id int NOT NULL REFERENCES places(place_id), UNIQUE (round_id, place_id) ); +CREATE INDEX contests_master_contest_id_index ON contests (master_contest_id); + -- Detaily účastníka CREATE TABLE participants ( user_id int NOT NULL REFERENCES users(user_id), diff --git a/db/upgrade-20210307b.sql b/db/upgrade-20210307b.sql new file mode 100644 index 0000000000000000000000000000000000000000..4c1fc444e11bac39a054bfd2b15ee3f0b4ffae81 --- /dev/null +++ b/db/upgrade-20210307b.sql @@ -0,0 +1,18 @@ +SET ROLE 'mo_osmo'; + +ALTER TABLE rounds + ADD COLUMN master_round_id int DEFAULT NULL REFERENCES rounds(round_id), + ADD COLUMN part int NOT NULL DEFAULT 0; -- část kola (u dělených kol) + +ALTER TABLE contests + ADD COLUMN master_contest_id int DEFAULT NULL REFERENCES contests(contest_id); + +ALTER TABLE rounds + DROP CONSTRAINT "rounds_year_category_seq_key", + ADD CONSTRAINT "rounds_year_category_seq_part_key" UNIQUE (year, category, seq, part); + +CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id); +CREATE INDEX contests_master_contest_id_index ON contests (master_contest_id); + +UPDATE rounds SET master_round_id=round_id; +UPDATE contests SET master_contest_id=contest_id; diff --git a/mo/db.py b/mo/db.py index 7ddf905cb220d7ec712e78c8f4c25310fd030920..769e839d8e6e251ea413d37bc984d35964387890 100644 --- a/mo/db.py +++ b/mo/db.py @@ -187,13 +187,15 @@ round_score_mode_names = { class Round(Base): __tablename__ = 'rounds' __table_args__ = ( - UniqueConstraint('year', 'category', 'seq'), + UniqueConstraint('year', 'category', 'seq', 'part'), ) round_id = Column(Integer, primary_key=True, server_default=text("nextval('rounds_round_id_seq'::regclass)")) + master_round_id = Column(Integer, ForeignKey('rounds.round_id')) year = Column(Integer, nullable=False) category = Column(String(2), nullable=False) seq = Column(Integer, nullable=False) + part = Column(Integer, nullable=False) level = Column(Integer, nullable=False) name = Column(String(255), nullable=False) state = Column(Enum(RoundState, name='round_state'), nullable=False, server_default=text("'preparing'::round_state")) @@ -206,12 +208,34 @@ class Round(Base): score_winner_limit = Column(Integer) score_successful_limit = Column(Integer) - def round_code(self): + master = relationship('Round', primaryjoin='Round.master_round_id == Round.round_id', remote_side='Round.round_id', post_update=True) + + def round_code_short(self): + """ Pro samostatné kolo ekvivalentní s `round_code()`, pro skupinu kol společná část kódu. """ return f"{self.year}-{self.category}-{self.seq}" + def part_code(self): + return chr(ord('a') + self.part - 1) if self.part > 0 else "" + + def round_code(self): + """ Kód kola včetně označení části, pokud je ve skupině kol. """ + code = self.round_code_short() + part = self.part_code() + return f"{code}{part}" + def has_tasks(self): return self.tasks_file + def is_subround(self) -> bool: + return self.master_round_id != self.round_id + + def get_group_rounds(self, add_self=False) -> List['Round']: + # podle PEP484 se má použít string při forward typové referenci + return get_session().query(Round).filter( + Round.master_round_id == self.master_round_id, + add_self or Round.round_id != self.round_id, + ).all() + def long_state(self) -> str: details = "" if self.state == RoundState.preparing: @@ -275,12 +299,23 @@ class Contest(Base): ) contest_id = Column(Integer, primary_key=True, server_default=text("nextval('contests_contest_id_seq'::regclass)")) + master_contest_id = Column(Integer, ForeignKey('contests.contest_id')) round_id = Column(Integer, ForeignKey('rounds.round_id'), nullable=False) place_id = Column(Integer, ForeignKey('places.place_id'), nullable=False) + master = relationship('Contest', primaryjoin='Contest.master_contest_id == Contest.contest_id', remote_side='Contest.contest_id', post_update=True) place = relationship('Place') round = relationship('Round') + def is_subcontest(self) -> bool: + return self.master_contest_id != self.contest_id + + def get_group_contests(self, add_self=False) -> List['Contest']: + # podle PEP484 se má použít string při forward typové referenci + return get_session().query(Contest).filter( + Contest.master_contest_id == self.master_contest_id, + add_self or Contest.contest_id != self.contest_id, + ).options(joinedload(Contest.round)).all() class LogType(MOEnum): general = auto() diff --git a/mo/jobs/submit.py b/mo/jobs/submit.py index 019b907402a1c5ebe48b257dd9603eaed06a68ae..3f40f738d52f5a2c1a6a711ae33f4b6595959ab1 100644 --- a/mo/jobs/submit.py +++ b/mo/jobs/submit.py @@ -51,7 +51,7 @@ def handle_download_submits(the_job: TheJob): .join(db.User, db.User.user_id == db.Paper.for_user) .join(db.Task, db.Task.task_id == db.Paper.for_task) .join(db.Participation, db.Participation.user_id == db.Paper.for_user) - .join(db.Contest, and_(db.Contest.contest_id == db.Participation.contest_id, db.Contest.round_id == db.Task.round_id)) + .join(db.Contest, and_(db.Contest.master_contest_id == db.Participation.contest_id, db.Contest.round_id == db.Task.round_id)) .join(db.Place, db.Place.place_id == db.Contest.place_id) .all()) papers.sort(key=lambda p: (p[1].sort_key(), p[2])) @@ -216,7 +216,7 @@ def handle_upload_feedback(the_job: TheJob): rows = (sess.query(db.User, db.Participation, db.Contest) .select_from(db.Participation) .join(db.User, db.User.user_id == db.Participation.user_id) - .join(db.Contest, db.Contest.contest_id == db.Participation.contest_id) + .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id) .filter(db.Contest.round == round) .filter(db.Participation.user_id.in_(user_dict.keys())) .all()) diff --git a/mo/score.py b/mo/score.py index b7f4ed5d36fbf561fc29d520d02dd50f3a4e6448..aba2ffe7517ffe8c92f4e21e9bbe1ff82896ebda 100644 --- a/mo/score.py +++ b/mo/score.py @@ -105,12 +105,12 @@ class Score: self.contest = contest self.part_states = part_states - # Příprava subquery na účastníky + # Příprava subquery na účastníky (contest_subq obsahuje master_contest_id) sess = db.get_session() if contest: - contest_subq = [contest.contest_id] + contest_subq = [contest.master_contest_id] else: - contest_subq = sess.query(db.Contest.contest_id).filter_by(round=round) + contest_subq = sess.query(db.Contest.master_contest_id).filter_by(round=round) # Načtení účastníků data: List[Tuple[db.User, db.Participation, db.Participant]] = ( @@ -164,7 +164,9 @@ class Score: num_participants = db.get_count(user_id_subq) # Načtení úloh - tasks: List[db.Task] = sess.query(db.Task).filter_by(round=round).all() + tasks: List[db.Task] = sess.query(db.Task).filter(db.Task.round_id.in_( + sess.query(db.Round.round_id).filter_by(master_round_id=round.master_round_id) + )).all() for task in tasks: self._tasks[step][task.task_id] = ScoreTask(task) self._tasks[step][task.task_id].num_solutions = num_participants @@ -202,7 +204,7 @@ class Score: # Zkusíme nalézt kolo o `step` kroků zpět prev_round = sess.query(db.Round).filter_by( year=self.round.year, category=self.round.category, seq=self.round.seq - step - ).one_or_none() + ).filter(db.Round.master_round_id == db.Round.round_id).one_or_none() if prev_round is None: return False self._prev_rounds[step] = prev_round @@ -211,13 +213,13 @@ class Score: # Pokud tvoříme výsledkovku pro contest, tak nás zajímají jen řešení # z podoblastí contestu spadajícího pod hlavní desc_cte = db.place_descendant_cte(self.contest.place, max_level=prev_round.level) - contest_subq = sess.query(db.Contest.contest_id).filter( + contest_subq = sess.query(db.Contest.master_contest_id).filter( db.Contest.round == prev_round, db.Contest.place_id.in_(select([desc_cte])) ) else: # Pokud vytváříme výsledkovku pro celé kolo, bereme vše - contest_subq = sess.query(db.Contest.contest_id).filter_by(round=prev_round) + contest_subq = sess.query(db.Contest.master_contest_id).filter_by(round=prev_round) self._load_tasks_and_sols(step, prev_round, contest_subq) return True diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 933d9d2ae77dfadc2b096c04f965988bd67279fb..d788d9a0cc9a192325b18362f7a6c87ff769ddce 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -15,7 +15,7 @@ import wtforms import mo from mo.csv import FileFormat import mo.db as db -from mo.imports import ImportType, Import, create_import +from mo.imports import ImportType, create_import import mo.jobs.submit from mo.rights import Right, Rights import mo.util @@ -129,13 +129,14 @@ class ParticipantsActionForm(FlaskForm): if not contest_place: flash("Nepovedlo se najít zadanou soutěžní oblast", 'danger') return False - contest = sess.query(db.Contest).filter_by(round_id=round.round_id, place_id=contest_place.place_id).one_or_none() + # Contest hledáme vždy v master kole, abychom náhodou nepřesunuli účastníky do soutěže v podkole + contest = sess.query(db.Contest).filter_by(round_id=round.master_round_id, place_id=contest_place.place_id).one_or_none() if not contest: - flash(f"Nepovedlo se najít soutěž v kole {round.round_code()} v oblasti {contest_place.name}", 'danger') + flash(f"Nepovedlo se najít soutěž v kole {round.round_code_short()} v oblasti {contest_place.name}", 'danger') return False rr = g.gatekeeper.rights_for_contest(contest) if not rr.have_right(Right.manage_contest): - flash(f"Nemáte právo ke správě soutěže v kole {round.round_code()} v oblasti {contest_place.name}, nelze do ní přesunout účastníky", 'danger') + flash(f"Nemáte právo ke správě soutěže v kole {round.round_code_short()} v oblasti {contest_place.name}, nelze do ní přesunout účastníky", 'danger') return False elif self.remove_participation.data: pass @@ -158,7 +159,7 @@ class ParticipantsActionForm(FlaskForm): rr = g.gatekeeper.rights_for_contest(pion.contest) if not rr.have_right(Right.manage_contest): flash( - f"Nemáte právo ke správě soutěže v kole {round.round_code()} v oblasti {pion.contest.place.name} " + f"Nemáte právo ke správě soutěže v kole {round.round_code_short()} v oblasti {pion.contest.place.name} " + f"(účastník {u.full_name()}). Žádná akce nebyla provedena.", 'danger' ) return False @@ -233,33 +234,43 @@ class ParticipantsActionForm(FlaskForm): return True -def get_contest(id: int) -> db.Contest: +def get_contest(id: int) -> Tuple[db.Contest, db.Contest]: + """ Vrací contest a master_contest pro zadané contest_id. + Pro nedělená kola platí contest == master_contest. + Operace s účastníky by měly probíhat vždy přes master_contest.""" contest = (db.get_session().query(db.Contest) .options(joinedload(db.Contest.place), - joinedload(db.Contest.round)) + joinedload(db.Contest.round), + joinedload(db.Contest.master).joinedload(db.Contest.round)) .get(id)) if not contest: raise werkzeug.exceptions.NotFound() - return contest + return contest, contest.master -def get_contest_rr(id: int, right_needed: Optional[Right] = None) -> Tuple[db.Contest, Rights]: - contest = get_contest(id) +def get_contest_rr(id: int, right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, Rights]: + """ Vrací contest, master_contest a Rights objekt pro zadané contest_id. + Pro nedělená kola platí contest == master_contest. + Operace s účastníky by měly probíhat vždy přes master_contest.""" + contest, master_contest = get_contest(id) rr = g.gatekeeper.rights_for_contest(contest) if not (right_needed is None or rr.have_right(right_needed)): raise werkzeug.exceptions.Forbidden() - return contest, rr + return contest, master_contest, rr -def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[Right] = None) -> Tuple[db.Contest, Optional[db.Place], Rights]: +def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, Optional[db.Place], Rights]: + """ Vrací contest, master_contest, optional site a Rights objekt pro zadané contest_id a site_id. + Pro nedělená kola platí contest == master_contest. + Operace s účastníky by měly probíhat vždy přes master_contest.""" if site_id is None: - contest, rr = get_contest_rr(id, right_needed) - return contest, None, rr + contest, master_contest, rr = get_contest_rr(id, right_needed) + return contest, master_contest, None, rr - contest = get_contest(id) + contest, master_contest = get_contest(id) site = db.get_session().query(db.Place).get(site_id) if not site: raise werkzeug.exceptions.NotFound() @@ -269,7 +280,7 @@ def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[ if not (right_needed is None or rr.have_right(right_needed)): raise werkzeug.exceptions.Forbidden() - return contest, site, rr + return contest, master_contest, site, rr def contest_breadcrumbs( @@ -313,11 +324,11 @@ def contest_breadcrumbs( @app.route('/org/contest/c/<int:id>/site/<int:site_id>/') def org_contest(id: int, site_id: Optional[int] = None): sess = db.get_session() - contest, site, rr = get_contest_site_rr(id, site_id, None) + contest, master_contest, site, rr = get_contest_site_rr(id, site_id, None) round = contest.round tasks_subq = sess.query(db.Task.task_id).filter_by(round=contest.round) - pions_subq = sess.query(db.Participation.user_id).filter_by(contest=contest) + pions_subq = sess.query(db.Participation.user_id).filter_by(contest=master_contest) if site: pions_subq = pions_subq.filter_by(place=site) sol_counts_q = ( @@ -343,12 +354,15 @@ def org_contest(id: int, site_id: Optional[int] = None): sess.query(db.Place, func.count('*')) .select_from(db.Participation).join(db.Place) .group_by(db.Place) - .filter(db.Participation.contest_id == id).all() + .filter(db.Participation.contest == master_contest).all() ) + group_contests = contest.get_group_contests(True) + group_contests.sort(key=lambda c: c.round.round_code()) + return render_template( 'org_contest.html', - contest=contest, site=site, + contest=contest, group_contests=group_contests, site=site, rights=sorted(rr.rights, key=lambda r: r.name), roles=[r.friendly_name() for r in rr.get_roles()], can_manage=rr.have_right(Right.manage_contest), @@ -360,7 +374,7 @@ def org_contest(id: int, site_id: Optional[int] = None): ) -def generic_import(round: db.Round, contest: Optional[db.Contest]): +def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db.Contest], master_contest: Optional[db.Contest]): """Společná funkce pro importování do soutěží a kol""" form = ImportForm() @@ -368,7 +382,7 @@ def generic_import(round: db.Round, contest: Optional[db.Contest]): warnings = [] if form.validate_on_submit(): fmt = form.fmt.data - imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=round, contest=contest) + imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=master_round, contest=master_contest) if form.submit.data: if form.file.data is not None: file = form.file.data.stream @@ -412,21 +426,24 @@ def doc_import(): @app.route('/org/contest/c/<int:id>/import', methods=('GET', 'POST')) def org_contest_import(id: int): - contest, rr = get_contest_rr(id, Right.manage_contest) - return generic_import(contest.round, contest) + contest, master_contest, rr = get_contest_rr(id, Right.manage_contest) + return generic_import( + round=contest.round, master_round=master_contest.round, + contest=contest, master_contest=master_contest + ) @app.route('/org/contest/c/<int:id>/ucastnici', methods=('GET', 'POST')) @app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici', methods=('GET', 'POST')) def org_contest_list(id: int, site_id: Optional[int] = None): - contest, site, rr = get_contest_site_rr(id, site_id) + contest, master_contest, site, rr = get_contest_site_rr(id, site_id) can_edit = rr.have_right(Right.manage_contest) format = request.args.get('format', "") filter = ParticipantsFilterForm(request.args) filter.validate() query = get_contestants_query( - round=contest.round, contest=contest, site=site, + round=master_contest.round, contest=master_contest, site=site, school=filter.f_school, # contest_place=filter.f_contest_place, participation_place=filter.f_participation_place, @@ -556,7 +573,9 @@ def make_contestant_table(query: Query, add_checkbox: bool = False, add_contest_ @dataclass class SolutionContext: contest: db.Contest + master_contest: db.Contest round: db.Round + master_round: db.Round pion: Optional[db.Participation] user: Optional[db.User] task: Optional[db.Task] @@ -572,8 +591,9 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio sess = db.get_session() # Nejprve zjistíme, zda existuje soutěž - contest = get_contest(contest_id) + contest, master_contest = get_contest(contest_id) round = contest.round + master_round = master_contest.round # Najdeme úlohu a ověříme, že je součástí soutěže if task_id is not None: @@ -588,7 +608,7 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio if user_id is not None: # Zkontrolujeme, zda se účastník opravdu účastní soutěže pion = (sess.query(db.Participation) - .filter_by(user_id=user_id, contest_id=contest_id) + .filter_by(user_id=user_id, contest_id=master_contest.contest_id) .options(joinedload(db.Participation.place), joinedload(db.Participation.user)) .one_or_none()) @@ -620,8 +640,8 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio allow_upload_solutions = rr.can_upload_solutions(round) allow_upload_feedback = rr.can_upload_feedback(round) return SolutionContext( - contest=contest, - round=round, + contest=contest, master_contest=master_contest, + round=round, master_round=master_round, pion=pion, user=user, task=task, @@ -934,7 +954,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non sess = db.get_session() - q = get_solutions_query(sc.task, for_contest=sc.contest, for_site=sc.site) + q = get_solutions_query(sc.task, for_contest=sc.master_contest, for_site=sc.site) rows: List[Tuple[db.Participation, db.Solution]] = q.all() rows.sort(key=lambda r: r[0].user.sort_key()) @@ -1044,13 +1064,13 @@ def org_contest_solutions(id: int, site_id: Optional[int] = None): if edit_action and not sc.allow_create_solutions: raise werkzeug.exceptions.Forbidden() - pions_subq = sess.query(db.Participation.user_id).filter_by(contest=sc.contest) + pions_subq = sess.query(db.Participation.user_id).filter_by(contest=sc.master_contest) if sc.site: pions_subq = pions_subq.filter_by(place=sc.site) pions_subq = pions_subq.subquery() pions = (sess.query(db.Participation) .filter( - db.Participation.contest == sc.contest, + db.Participation.contest == sc.master_contest, db.Participation.user_id.in_(pions_subq), ).options(joinedload(db.Participation.user)) .all()) @@ -1182,11 +1202,11 @@ def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site: pion_query = sess.query(db.Participation.user_id).select_from(db.Participation) if contest is not None: - pion_query = pion_query.filter_by(contest=contest) + pion_query = pion_query.filter_by(contest_id=contest.master_contest_id) if site is not None: pion_query = pion_query.filter_by(place=site) else: - pion_query = pion_query.join(db.Contest).filter(db.Contest.round == round) + pion_query = pion_query.join(db.Contest).filter(db.Contest.round_id == round.master_round_id) sol_query = (sess.query(db.Solution) .select_from(db.Solution) @@ -1394,7 +1414,7 @@ class AdvanceForm(FlaskForm): def org_contest_advance(contest_id: int): sess = db.get_session() conn = sess.connection() - contest, rr = get_contest_rr(contest_id, Right.manage_contest) + contest, master_contest, rr = get_contest_rr(contest_id, Right.manage_contest) def redirect_back(): return redirect(url_for('org_contest', id=contest_id)) @@ -1404,11 +1424,14 @@ def org_contest_advance(contest_id: int): flash('Aktuální kolo není ve stavu přípravy', 'danger') return redirect_back() - prev_round = sess.query(db.Round).filter_by(year=round.year, category=round.category, seq=round.seq - 1).one_or_none() + prev_round = sess.query(db.Round).filter_by( + year=round.year, category=round.category, seq=round.seq - 1 + ).filter(db.Round.master_round_id == db.Round.round_id).one_or_none() if prev_round is None: flash('Předchozí kolo nenalezeno', 'danger') return redirect_back() elif prev_round.state != db.RoundState.closed: + # FIXME: Možná kontrolovat stav uzavření všech kol ve skupině kol? flash('Předchozí kolo dosud nebylo ukončeno', 'danger') return redirect_back() elif prev_round.level < round.level: @@ -1440,7 +1463,8 @@ def org_contest_advance(contest_id: int): if form.boundary.data > 0: accept_uids = (sess.query(db.Solution.user_id) .select_from(db.Solution) - .join(db.Task, and_(db.Task.task_id == db.Solution.task_id, db.Task.round == prev_round)) + # Vybíráme úlohy, jejich round patří do stejné skupiny kol jako prev_round + .join(db.Task, and_(db.Task.task_id == db.Solution.task_id, db.Task.round.master_round_id == prev_round.round_id)) .filter(db.Solution.user_id.in_(prev_pion_query.with_entities(db.Participation.user_id).subquery())) .group_by(db.Solution.user_id) .having(func.sum(db.Solution.points) >= form.boundary.data) @@ -1451,10 +1475,10 @@ def org_contest_advance(contest_id: int): want_execute = form.execute.data if want_execute: - app.logger.info(f'Postup: Z kola #{prev_round.round_id} do #{round.round_id}, soutěž #{contest_id}') + app.logger.info(f'Postup: Z kola #{prev_round.round_id} do #{round.master_round_id}, soutěž #{master_contest.contest_id}') mo.util.log( type=db.LogType.contest, - what=contest_id, + what=master_contest.contest_id, details={'action': 'advance'}, ) diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 9235d248ed68d91fa94dc4be8b668b58899486b6..f14705d3b4aa9d138d7079279e0592147457743a 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -20,22 +20,20 @@ from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, g generic_import, generic_batch_download, generic_batch_upload, generic_batch_points -def get_round(id: int) -> db.Round: - round = db.get_session().query(db.Round).get(id) +def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tuple[db.Round, db.Round, Rights]: + """Vrací round, master_round a Rights objekt pro zadané round_id. + Pro nedělená kola platí round == master_round. + Operace s účastníky by měly probíhat vždy přes master_round.""" + round = db.get_session().query(db.Round).options(joinedload(db.Round.master)).get(id) if not round: raise werkzeug.exceptions.NotFound() - return round - - -def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tuple[db.Round, Rights]: - round = get_round(id) rr = g.gatekeeper.rights_for_round(round, any_place) if not (right_needed is None or rr.have_right(right_needed)): raise werkzeug.exceptions.Forbidden() - return round, rr + return round, round.master, rr def get_task(round: db.Round, task_id: int) -> db.Task: @@ -49,7 +47,7 @@ def get_task(round: db.Round, task_id: int) -> db.Task: def org_rounds(): sess = db.get_session() - rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq) + rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq, db.Round.part) return render_template('org_rounds.html', rounds=rounds, level_names=mo.db.place_level_names) @@ -104,16 +102,17 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: flash(f'Místo s kódem {form.place_code.data} neexistuje', 'danger') return False - if place.level != round.level: - flash(f'{place.type_name().title()} {place.name} není {db.place_level_names[round.level]}', 'danger') - return False + # if place.level != round.level: + # flash(f'{place.type_name().title()} {place.name} není {db.place_level_names[round.level]}', 'danger') + # return False sess = db.get_session() if sess.query(db.Contest).filter_by(round=round, place=place).one_or_none(): flash(f'Pro {place.type_name()} {place.name} už toto kolo existuje', 'danger') return False - contest = db.Contest(round=round, place=place) + # Soutěž vytvoříme vždy v hlavním kole + contest = db.Contest(round=round.master, place=place) rr = g.gatekeeper.rights_for_contest(contest) if not rr.have_right(Right.add_contest): flash('Vaše role nedovoluje vytvořit soutěž v oblasti {place.type_name()} {place.name}', 'danger') @@ -121,6 +120,9 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: sess.add(contest) sess.flush() + contest.master_contest_id = contest.contest_id + sess.add(contest) + sess.flush() mo.util.log( type=db.LogType.contest, @@ -128,8 +130,26 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: details={'action': 'add', 'contest': db.row2dict(contest)}, ) app.logger.info(f"Soutěž #{contest.contest_id} založena: {db.row2dict(contest)}") - sess.commit() + # Přidání soutěže do podkol ve skupině + subrounds = round.master.get_group_rounds() + for subround in subrounds: + subcontest = db.Contest( + round_id=subround.round_id, + master_contest_id=contest.contest_id, + place_id=contest.place_id, + ) + sess.add(subcontest) + sess.flush() + + mo.util.log( + type=db.LogType.contest, + what=subcontest.contest_id, + details={'action': 'add', 'contest': db.row2dict(subcontest)}, + ) + app.logger.info(f"Soutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}") + + sess.commit() flash(f'Soutěž v oblasti {place.type_name()} {place.name} založena', 'success') return True @@ -137,7 +157,7 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: @app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST')) def org_round(id: int): sess = db.get_session() - round, rr = get_round_rr(id, None, True) + round, _, rr = get_round_rr(id, None, True) can_manage_round = rr.have_right(Right.manage_round) can_manage_contestants = rr.have_right(Right.manage_contest) @@ -147,10 +167,11 @@ def org_round(id: int): func.count(db.Participation.user_id).label('count') ).group_by(db.Participation.contest_id).subquery() + # účastníci jsou jen pod master contesty contests_counts = (sess.query( db.Contest, coalesce(participants_count.c.count, 0) - ).outerjoin(participants_count) + ).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id) .filter(db.Contest.round == round) .options(joinedload(db.Contest.place)) .all()) @@ -181,9 +202,12 @@ def org_round(id: int): if add_contest(round, form_add_contest): return redirect(url_for('org_round', id=id)) + group_rounds = round.get_group_rounds(True) + group_rounds.sort(key=lambda r: r.round_code()) + return render_template( 'org_round.html', - round=round, + round=round, group_rounds=group_rounds, roles=[r.friendly_name() for r in rr.get_roles()], contests_counts=contests_counts, tasks=tasks, form_delete_task=form_delete_task, @@ -215,7 +239,7 @@ class TaskEditForm(FlaskForm): @app.route('/org/contest/r/<int:id>/task/new', methods=('GET', 'POST')) def org_round_task_new(id: int): sess = db.get_session() - round, rr = get_round_rr(id, Right.manage_round, True) + round, _, _ = get_round_rr(id, Right.manage_round, True) form = TaskEditForm() if form.validate_on_submit(): @@ -247,7 +271,7 @@ def org_round_task_new(id: int): @app.route('/org/contest/r/<int:id>/task/<int:task_id>/edit', methods=('GET', 'POST')) def org_round_task_edit(id: int, task_id: int): sess = db.get_session() - round, rr = get_round_rr(id, Right.manage_round, True) + round, _, _ = get_round_rr(id, Right.manage_round, True) task = sess.query(db.Task).get(task_id) # FIXME: Check contest! @@ -286,14 +310,14 @@ def org_round_task_edit(id: int, task_id: int): @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST')) def org_round_task_download(round_id: int, task_id: int): - round, rr = get_round_rr(round_id, Right.view_submits, False) + round, _, _ = get_round_rr(round_id, Right.view_submits, False) task = get_task(round, task_id) return generic_batch_download(round=round, contest=None, site=None, task=task) @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) def org_round_task_upload(round_id: int, task_id: int): - round, rr = get_round_rr(round_id, Right.view_submits, False) + round, _, rr = get_round_rr(round_id, Right.view_submits, False) task = get_task(round, task_id) return generic_batch_upload(round=round, contest=None, site=None, task=task, can_upload_solutions=rr.can_upload_solutions(round), @@ -302,20 +326,20 @@ def org_round_task_upload(round_id: int, task_id: int): @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST')) def org_round_task_batch_points(round_id: int, task_id: int): - round, rr = get_round_rr(round_id, Right.edit_points, True) + round, _, _ = get_round_rr(round_id, Right.edit_points, True) task = get_task(round, task_id) return generic_batch_points(round=round, contest=None, task=task) @app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST')) def org_round_list(id: int): - round, rr = get_round_rr(id, Right.manage_contest, True) + round, master_round, r = get_round_rr(id, Right.manage_contest, True) format = request.args.get('format', "") filter = ParticipantsFilterForm(request.args) filter.validate() query = get_contestants_query( - round=round, + round=master_round, school=filter.f_school, contest_place=filter.f_contest_place, participation_place=filter.f_participation_place, @@ -323,7 +347,7 @@ def org_round_list(id: int): ) action_form = ParticipantsActionForm() - if action_form.do_action(round=round, query=query): + if action_form.do_action(round=master_round, query=query): # Action happened, redirect return redirect(request.url) @@ -345,8 +369,8 @@ def org_round_list(id: int): @app.route('/org/contest/r/<int:id>/import', methods=('GET', 'POST')) def org_round_import(id: int): - round, rr = get_round_rr(id, Right.manage_contest, True) - return generic_import(round, None) + round, master_round, rr = get_round_rr(id, Right.manage_contest, True) + return generic_import(round, master_round, None, None) class MODateTimeField(wtforms.DateTimeField): @@ -355,7 +379,7 @@ class MODateTimeField(wtforms.DateTimeField): super().__init__(label, format=format, description=description, **kwargs) def process_data(self, valuelist): - super().process_formdata(valuelist) + super().process_data(valuelist) if self.data is not None: self.data = self.data.astimezone() @@ -366,6 +390,7 @@ class MODateTimeField(wtforms.DateTimeField): class RoundEditForm(FlaskForm): + name = wtforms.StringField("Název") state = wtforms.SelectField("Stav kola", choices=db.RoundState.choices(), coerce=db.RoundState.coerce) # Only the desktop Firefox does not support datetime-local field nowadays, # other browsers does provide date and time picker UI :( @@ -389,7 +414,7 @@ class RoundEditForm(FlaskForm): @app.route('/org/contest/r/<int:id>/edit', methods=('GET', 'POST')) def org_round_edit(id: int): sess = db.get_session() - round, rr = get_round_rr(id, Right.manage_round, True) + round, _, rr = get_round_rr(id, Right.manage_round, True) form = RoundEditForm(obj=round) if form.validate_on_submit(): @@ -420,7 +445,7 @@ def org_round_edit(id: int): @app.route('/org/contest/r/<int:id>/task-statement/zadani.pdf') def org_task_statement(id: int): - round, rr = get_round_rr(id, None, True) + round, _, rr = get_round_rr(id, None, True) if not rr.can_view_statement(round): app.logger.warn(f'Organizátor #{g.user.user_id} chce zadání, na které nemá právo') diff --git a/mo/web/org_score.py b/mo/web/org_score.py index 4925de52983b4623069b642da1d8207bc0d6bc4a..190e5e44911f393116c8f34d6597b0c15a440b87 100644 --- a/mo/web/org_score.py +++ b/mo/web/org_score.py @@ -1,6 +1,6 @@ from flask import render_template, request, g from flask.helpers import url_for -from typing import Optional +from typing import List, Optional import werkzeug.exceptions import mo @@ -102,6 +102,16 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): results = score.get_sorted_results() messages = score.get_messages() + # Pro tvorbu odkazů na správné contesty ve výsledkovkách dělených kol + all_subcontests: List[db.Contest] = sess.query(db.Contest).filter( + db.Contest.round_id.in_( + sess.query(db.Round.round_id).filter_by(master_round_id=round.master_round_id) + ) + ).all() + subcontest_id_map = {} + for subcontest in all_subcontests: + subcontest_id_map[(subcontest.round_id, subcontest.master_contest_id)] = subcontest.contest_id + # Construct columns is_export = (format != "") columns = [] @@ -123,13 +133,14 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): for task in tasks: title = task.code if contest_id: + local_ct_id = subcontest_id_map[(task.round_id, contest.master_contest_id)] title = '<a href="{}">{}</a>'.format( - url_for('org_contest_task', contest_id=contest_id, task_id=task.task_id), + url_for('org_contest_task', contest_id=local_ct_id, task_id=task.task_id), task.code ) if rr.can_edit_points(round): title += ' <a href="{}" title="Editovat body" class="icon">✎</a>'.format( - url_for('org_contest_task_points', contest_id=contest_id, task_id=task.task_id), + url_for('org_contest_task_points', contest_id=local_ct_id, task_id=task.task_id), ) columns.append(Column(key=f'task_{task.task_id}', name=task.code, title=title)) columns.append(Column(key='total_points', name='celkove_body', title='Celkové body')) @@ -140,13 +151,14 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): for result in results: user, pant, pion = result.user, result.pant, result.pion school = pant.school_place + local_pion_ct_id = subcontest_id_map[(round.round_id, pion.contest_id)] row = Row(keys={ 'order': OrderCell(result.order.place, result.order.span, result.order.continuation), 'winner': 'ano' if result.winner else '', 'successful': 'ano' if result.successful else '', 'user': user, 'email': user.email, - 'participant': cell_pion_link(user, pion.contest_id, user.full_name()), + 'participant': cell_pion_link(user, local_pion_ct_id, user.full_name()), 'contest': CellLink(pion.contest.place.name, url_for('org_contest', id=pion.contest_id)), 'pion_place': pion.place.name, 'school': CellLink(school.name, url_for('org_place', id=school.place_id)), @@ -157,8 +169,9 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): }) sols = result.get_sols_map() for task in tasks: + local_sol_ct_id = subcontest_id_map[(task.round_id, pion.contest_id)] row.keys[f'task_{task.task_id}'] = SolPointsCell( - contest_id=pion.contest_id, user=user, sol=sols.get(task.task_id) + contest_id=local_sol_ct_id, user=user, sol=sols.get(task.task_id) ) if result.winner: row.html_attr = {"class": "winner", "title": "Vítěz"} @@ -176,11 +189,15 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): filename=filename, ) + group_rounds = round.get_group_rounds(True) + group_rounds.sort(key=lambda r: r.round_code()) + if format == "": return render_template( 'org_score.html', contest=contest, round=round, tasks=tasks, table=table, messages=messages, + group_rounds=group_rounds, ) else: return table.send_as(format) diff --git a/mo/web/org_users.py b/mo/web/org_users.py index 1a9fffa2e9838cae834c2a7bf241a1c85314ccfc..5937abc2cd8c1c17e5b9e2ca9d89d8e582db3642 100644 --- a/mo/web/org_users.py +++ b/mo/web/org_users.py @@ -301,12 +301,21 @@ def org_user(id: int): rr = g.gatekeeper.rights_generic() participants = sess.query(db.Participant).filter_by(user_id=user.user_id) - rounds = sess.query(db.Participation).filter_by(user_id=user.user_id) + participations = ( + sess.query(db.Participation, db.Contest, db.Round) + .select_from(db.Participation) + .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id) + .join(db.Round) + .filter(db.Participation.user == user) + .options(joinedload(db.Contest.place)) + .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part) + .all() + ) return render_template( 'org_user.html', user=user, can_edit=rr.can_edit_user(user), can_incarnate=g.user.is_admin, - participants=participants, rounds=rounds + participants=participants, participations=participations, ) diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index afed4ec18db6334194cd06b8f18d07e7de922a64..5a1cc62b44445c7e83117bfd84c20deb6f579faa 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -18,6 +18,14 @@ {% endif %} <tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }} <tr><td>Vaše role<td>{% if roles %}{{ roles|join(", ") }}{% else %}–{% endif %} + {% if group_contests|length > 1 %} + <tr><td>Soutěže ve skupině kol:<td> + {% for c in group_contests %} + {% if c == contest %}<i>{% else %}<a href="{{ url_for('org_contest', id=c.contest_id) }}">{% endif %} + {{ c.round.round_code() }}: {% if site %}soutěžní místo {{ site.name }}{% else %}{{ contest.place.name }}{% endif %} + {% if c == contest %}</i>{% else %}</a>{% endif %}<br> + {% endfor %} + {% endif %} <tr><td>Zadání<td> {% if round.tasks_file %} {% if can_view_statement %} diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html index dcb73a6f713c7b2386521b024525a0d7ee0ba0d6..623a82bc69c9b95e6d4ea27a5996c4bed16cf1da 100644 --- a/mo/web/templates/org_contest_solutions.html +++ b/mo/web/templates/org_contest_solutions.html @@ -61,7 +61,7 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% for pion in pions %} {% set u = pion.user %} <tr class="state-{{ pion.state.name }}{% if u.is_test %} testuser{% endif %}" {% if u.is_test %}title="Testovací uživatel"{% endif %}> - <th>{{ u|pion_link(pion.contest_id) }} + <th>{{ u|pion_link(contest.contest_id) }} <td>{{ pion.state.friendly_name() }} {% set sum_points = [] %} {% for task in tasks %} diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html index f8109b80a85e7ed910240225a6f5ee5de95d8820..9f4a4055be4ea6e2f02a5fad8e7ee77aab0e8b00 100644 --- a/mo/web/templates/org_round.html +++ b/mo/web/templates/org_round.html @@ -12,10 +12,20 @@ <tr><td>Ročník<td>{{ round.year }} <tr><td>Kategorie<td>{{ round.category }} <tr><td>Pořadí<td>{{ round.seq }} - <tr><td>Název<td>{{ round.name }} + {% if round.part > 0 %}<tr><td>Část:<td>{{ round.part_code() }}{% endif %} <tr><td>Oblast<td>{{ level_names[round.level] }} - <tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }} <tr><td>Vaše role<td>{% if roles %}{{ roles|join(", ") }}{% else %}–{% endif %} + {% if group_rounds|length > 1 %} + <tr><td>Skupina kol:<td> + {% for r in group_rounds %} + {% if r == round %}<i>{{ r.name }} {{ r.round_code() }} (toto kolo)</i><br> + {% else %}<a href="{{ url_for('org_round', id=r.round_id) }}">{{ r.name }} {{ r.round_code() }}</a><br> + {% endif %} + {% endfor %} + {% endif %} + <tr><th colspan=2><h4>Nastavení kola:</h4> + <tr><td>Název<td>{{ round.name }} + <tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }} <tr><td>Účastníci vidí zadání od<td>{{ round.ct_tasks_start|timeformat }} <tr><td>Účastníci odevzdávají do<td>{{ round.ct_submit_end|timeformat }} <tr><td>Dozor vidí zadání od<td>{{ round.pr_tasks_start|timeformat }} @@ -119,7 +129,7 @@ </div> {% endif %} {% if can_handle_submits or can_upload %} - <td><dic class="btn-group"> + <td><div class="btn-group"> {% if can_handle_submits %} <a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_download', round_id=round.round_id, task_id=task.task_id) }}">Stáhnout ZIP</a> {% endif %} diff --git a/mo/web/templates/org_rounds.html b/mo/web/templates/org_rounds.html index 9f7a261898001e57397aa8df10544452f09d66fa..27e362aff57d83492a95a73d16923796199229b6 100644 --- a/mo/web/templates/org_rounds.html +++ b/mo/web/templates/org_rounds.html @@ -17,7 +17,7 @@ <td><a href='{{ url_for('org_round', id=r.round_id) }}'>{{ r.round_code() }}</a> <td>{{ r.year }} <td>{{ r.category }} - <td>{{ r.seq }} + <td>{{ r.seq }}{{ r.part_code() }} <td>{{ level_names[r.level] }} <td>{{ r.name }} <td class='rstate-{{r.state.name}}'>{{ r.state.friendly_name() }} diff --git a/mo/web/templates/org_score.html b/mo/web/templates/org_score.html index 15ae4bebd5e42a2953787808516afefd50b9be27..8c1872548411d8d3387e6c4f88546f91da0a4372 100644 --- a/mo/web/templates/org_score.html +++ b/mo/web/templates/org_score.html @@ -38,6 +38,12 @@ </div> {% endif %} +{% if group_rounds|length > 1 %} +<p>Toto je <b>sdílená výsledková listina</b> pro několik kol: +{% for r in group_rounds %}{% if loop.index > 1 %}, {% endif %}<a href="{{ url_for('org_round', id=r.round_id) }}">{{ r.round_code() }} {{ r.name }}</a>{% endfor %}. +Jsou v ní započítány body ze všech úloh těchto kol.</p> +{% endif %} + <p>Mód této výsledkové listiny je <b>{{ round.score_mode.friendly_name() }}</b>. Diskvalifikovaní, odmítnuvší a nepřítomní účastníci jsou skryti, stejně tak testovací uživatelé. Export pod tabulkou obsahuje sloupce navíc. diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html index b6160543a667411b87759eed5977dea5a94f5204..83423526c2ea907f22466e2945117910aabb51e6 100644 --- a/mo/web/templates/org_user.html +++ b/mo/web/templates/org_user.html @@ -52,27 +52,27 @@ {% endif %} <h3>Účast v kolech</h3> -{% if rounds.count() %} +{% if participations %} <table class="data full"> <thead> <tr> <th>Ročník<th>Kategorie<th>Kolo<th>Místo<th>Stav účasti<th>Odkazy </tr> </thead> -{% for round in rounds %} +{% for (pion, contest, round) in participations %} <tr> - <td>{{ round.contest.round.year }} - <td>{{ round.contest.round.category }} - <td>{{ round.contest.round.seq }} - <td><a href="{{ url_for('org_place', id=round.contest.place_id) }}">{{ round.contest.place.name }}</a> - {% if round.place_id != round.contest.place_id %} - <br>(ale soutěží v <a href="{{ url_for('org_place', id=round.place_id) }}">{{ round.place.name }}</a>) + <td>{{ round.year }} + <td>{{ round.category }} + <td>{{ round.seq }}{{ round.part_code() }} + <td><a href="{{ url_for('org_place', id=contest.place_id) }}">{{ contest.place.name }}</a> + {% if pion.place_id != contest.place_id %} + <br>(ale soutěží v <a href="{{ url_for('org_place', id=pion.place_id) }}">{{ pion.place.name }}</a>) {% endif %} <td>{{ round.state.friendly_name() }} <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_user', contest_id=round.contest.contest_id, user_id=user.user_id) }}">Odevzdané úlohy</a> - <a class="btn btn-xs btn-default" href="{{ url_for('org_contest', id=round.contest.contest_id) }}">Stránka soutěže</a> - <a class="btn btn-xs btn-default" href="{{ url_for('org_round', id=round.contest.round.round_id) }}">Stránka kola</a> + <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_user', contest_id=contest.contest_id, user_id=user.user_id) }}">Odevzdané úlohy</a> + <a class="btn btn-xs btn-default" href="{{ url_for('org_contest', id=contest.contest_id) }}">Stránka soutěže</a> + <a class="btn btn-xs btn-default" href="{{ url_for('org_round', id=round.round_id) }}">Stránka kola</a> {% if g.user.is_admin %} <a class="btn btn-xs btn-default" href="{{ log_url('participant', user.user_id) }}">Historie</a> {% endif %} diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index a9340f07d23895a0acdda8be4bfe5ef4d86cf693..3b9c625c98bce7448602b47de839301eebf8a19a 100644 --- a/mo/web/templates/parts/org_solution_table.html +++ b/mo/web/templates/parts/org_solution_table.html @@ -41,7 +41,7 @@ finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solut <td>{% if for_user %} <a href='{{ url_for('org_contest_task', contest_id=ct_id, task_id=task.task_id) }}'>{{ task.code }} {{ task.name }}</a> {% else %} - {{ u|pion_link(obj.contest_id) }}</a> + {{ u|pion_link(contest.contest_id) }}</a> {% endif %} {% if for_task %}<td>{{ obj.state.friendly_name() }}{% endif %} {% if sol %} diff --git a/mo/web/user.py b/mo/web/user.py index f4ac0297b3e72e73939b74ce80e9676319ac14bf..5ebc54f2f02a4d563747a16c48f07014112da557 100644 --- a/mo/web/user.py +++ b/mo/web/user.py @@ -22,11 +22,11 @@ def user_index(): pions = (sess.query(db.Participation, db.Contest, db.Round) .select_from(db.Participation) - .join(db.Contest) + .join(db.Contest, db.Contest.master_contest_id == db.Participation.contest_id) .join(db.Round) .filter(db.Participation.user == g.user) .options(joinedload(db.Contest.place)) - .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq) + .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part) .all()) return render_template( @@ -46,7 +46,7 @@ def get_contest(id: int) -> db.Contest: # FIXME: Kontrolovat nějak pion.state? pion = (db.get_session().query(db.Participation) - .filter_by(user=g.user, contest=contest) + .filter_by(user=g.user, contest_id=contest.master_contest_id) .one_or_none()) if not pion: raise werkzeug.exceptions.Forbidden()