diff --git a/bin/create-contests b/bin/create-contests index 17c7573fe34fb536b6f4c8d5f6d80a503649c933..a8e7ac694742d9107834f8882c5da44f37d0f6d3 100755 --- a/bin/create-contests +++ b/bin/create-contests @@ -2,14 +2,15 @@ import argparse +from mo.arg_attrs import CONTEST_ATTRS, HelpFormatter import mo.db as db import mo.util from mo.util import die, init_standalone -parser = argparse.ArgumentParser(description='Založí soutěže pro dané kolo') +parser = argparse.ArgumentParser(description='Založí soutěže pro dané kolo', formatter_class=HelpFormatter) parser.add_argument(dest='round', type=str, metavar='YY-C-S[p]', help='kód kola') parser.add_argument('-r', '--region', type=str, metavar='CODE', help='soutěžní oblast (default: založit všechny)') -parser.add_argument('--online-submit', default=False, action='store_true', help='povolí elektronické odevzdávání') +CONTEST_ATTRS.add_to_arg_parser(parser) parser.add_argument('-n', '--dry-run', default=False, action='store_true', help='pouze ukáže, co by bylo provedeno') args = parser.parse_args() @@ -24,7 +25,8 @@ round = mo.util.get_round_by_code(round_code) if round is None: die("Kolo s tímto kódem neexistuje!") -online_submit = round.online_submit or args.online_submit +if args.online_submit is Ellipsis: + args.online_submit = round.online_submit region = None if args.region is not None: @@ -42,7 +44,8 @@ if round.is_subround(): print(f"{round.round_code()} pro místo {r.name}: zakládám (podsoutěž)") if not args.dry_run: - c = db.Contest(round=round, place=r, master=mc, state=round.last_state, online_submit=online_submit) + c = db.Contest(round=round, place=r, master=mc, state=round.last_state) + CONTEST_ATTRS.args_to_obj(args, c) sess.add(c) sess.flush() @@ -50,6 +53,7 @@ if round.is_subround(): mo.util.log(db.LogType.contest, c.contest_id, { 'action': 'created', 'reason': 'script', + 'new': db.row2dict(c), }) else: @@ -65,7 +69,8 @@ else: else: print(f"{round.round_code()} pro místo {r.name}: zakládám") if not args.dry_run: - c = db.Contest(round=round, place=r, state=round.last_state, online_submit=online_submit) + c = db.Contest(round=round, place=r, state=round.last_state) + CONTEST_ATTRS.args_to_obj(args, c) sess.add(c) sess.flush() @@ -74,6 +79,7 @@ else: mo.util.log(db.LogType.contest, c.contest_id, { 'action': 'created', 'reason': 'script', + 'new': db.row2dict(c), }) if not args.dry_run: diff --git a/bin/create-round b/bin/create-round index 6059dbc4316e59add5a52c638b75aa90d694f7f8..454f568d5b94e054bff050051462d0577e3235a7 100755 --- a/bin/create-round +++ b/bin/create-round @@ -2,27 +2,23 @@ import argparse +from mo.arg_attrs import ROUND_ATTRS, HelpFormatter import mo.db as db import mo.util from mo.util import die -parser = argparse.ArgumentParser(description='Založí soutěžní kolo') +parser = argparse.ArgumentParser(description='Založí soutěžní kolo', formatter_class=HelpFormatter) parser.add_argument('-y', '--year', type=int, required=True, help='ročník') parser.add_argument('-c', '--cat', type=str, required=True, help='kategorie') parser.add_argument('-s', '--seq', type=int, required=True, help='pořadí kola') -parser.add_argument('-l', '--level', type=int, required=True, help='úroveň v hierarchii oblastí') parser.add_argument('-p', '--part', type=int, default=0, help='část v rámci skupiny kol (default: 0)') -parser.add_argument('-n', '--name', type=str, required=True, help='název kola') -parser.add_argument('-S', '--step', type=float, default=1, help='bodovací krok (default: 1)') -parser.add_argument('-t', '--type', type=str, default='other', help='typ kola (default: other)') -parser.add_argument('--score-mode', type=str, default='basic', help='režim výsledkové listiny (default: basic)') -parser.add_argument('--enroll-mode', type=str, default='manual', help='režim registrace (default: manual)') -parser.add_argument('--enroll-advert', type=str, default='', help='popis v přihlášce') -parser.add_argument('--publish-score', default=False, action='store_true', help='publikovat výsledkové listiny') -parser.add_argument('--online-submit', default=False, action='store_true', help='povolit elektronické odevzdávání') +ROUND_ATTRS.add_to_arg_parser(parser) args = parser.parse_args() +if args.points_step is Ellipsis: + args.points_step = 1 + mo.util.init_standalone() sess = db.get_session() @@ -40,17 +36,10 @@ rnd = db.Round( category=args.cat, seq=args.seq, part=args.part, - level=args.level, - name=args.name, - points_step=args.step, - round_type=db.RoundType.coerce(args.type), - score_mode=db.RoundScoreMode.coerce(args.score_mode), - enroll_mode=db.RoundEnrollMode.coerce(args.enroll_mode), - enroll_advert=args.enroll_advert, - export_score_to_mo_web=args.publish_score, - online_submit=args.online_submit, ) +ROUND_ATTRS.args_to_obj(args, rnd) + sess.add(rnd) sess.flush() diff --git a/bin/set-contest b/bin/set-contest new file mode 100755 index 0000000000000000000000000000000000000000..bd711172f3d2cd557650955aa1c9dd760cef71f3 --- /dev/null +++ b/bin/set-contest @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +import argparse + +from mo.arg_attrs import CONTEST_ATTRS, HelpFormatter +import mo.db as db +import mo.util +from mo.util import die + + +parser = argparse.ArgumentParser(description='Nastaví parametry soutěží', formatter_class=HelpFormatter) +parser.add_argument('--in-round', type=str, metavar='YY-C-S[p]', help='nastaví všem soutěžím v daném kole') +parser.add_argument('--id', type=int, metavar='ID', help='nastaví soutěži s daným ID') +CONTEST_ATTRS.add_to_arg_parser(parser) + +args = parser.parse_args() + +mo.util.init_standalone() +sess = db.get_session() + +if args.in_round is not None: + round_code = mo.util.RoundCode.parse(args.in_round) + if round_code is None: + die("Chybná syntaxe kódu kola") + rnd = mo.util.get_round_by_code(round_code) + if rnd is None: + die("Kolo s tímto kódem neexistuje!") + contests = sess.query(db.Contest).filter_by(round=rnd).all() +elif args.id is not None: + contests = sess.query(db.Contest).filter_by(contest_id=args.id).all() + if not contests: + die("Soutěž s tímto ID neexistuje") +else: + die("Není vybraná žádná soutěž") + +num_modified = 0 + +for c in contests: + CONTEST_ATTRS.args_to_obj(args, c) + + if sess.is_modified(c): + num_modified += 1 + changes = db.get_object_changes(c) + mo.util.log( + type=db.LogType.contest, + what=c.contest_id, + details={ + 'action': 'edit', + 'reason': 'script', + 'changes': changes, + }, + ) + +sess.commit() +print(f'Modifikováno {num_modified} soutěží z {len(contests)}') diff --git a/bin/set-round b/bin/set-round new file mode 100755 index 0000000000000000000000000000000000000000..3d18c55c8f253afdd7681dd85a179acbe9be748a --- /dev/null +++ b/bin/set-round @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import argparse + +from mo.arg_attrs import ROUND_ATTRS, HelpFormatter +import mo.db as db +import mo.util +from mo.util import die + + +parser = argparse.ArgumentParser(description='Nastaví parametry soutěžního kola', formatter_class=HelpFormatter) +parser.add_argument(dest='round', type=str, metavar='YY-C-S[p]', help='kód kola') +ROUND_ATTRS.add_to_arg_parser(parser) + +args = parser.parse_args() + +mo.util.init_standalone() +sess = db.get_session() + +round_code = mo.util.RoundCode.parse(args.round) +if round_code is None: + die("Chybná syntaxe kódu kola") +rnd = mo.util.get_round_by_code(round_code) +if rnd is None: + die("Kolo s tímto kódem neexistuje!") + +ROUND_ATTRS.args_to_obj(args, rnd) + +if sess.is_modified(rnd): + changes = db.get_object_changes(rnd) + + mo.util.log( + type=db.LogType.round, + what=rnd.round_id, + details={ + 'action': 'edit', + 'reason': 'script', + 'changes': changes, + }, + ) + + sess.commit() diff --git a/mo/arg_attrs.py b/mo/arg_attrs.py new file mode 100644 index 0000000000000000000000000000000000000000..0db755364a85fb9448be9ea1382f0464856851b5 --- /dev/null +++ b/mo/arg_attrs.py @@ -0,0 +1,141 @@ +# Automatický generátor command-line argumentů pro atributy databázových objektů + +import argparse +from dataclasses import dataclass +from datetime import datetime +from typing import Callable, Any, Optional, Type, List + +import mo.db as db + + +class HelpFormatter(argparse.HelpFormatter): + + def __init__(self, *args, **kwargs): + super().__init__(*args, max_help_position=50, **kwargs) + + +@dataclass +class Attr: + name: str + parser: Callable[[str], Any] + help: str + long: Optional[str] = None + short: Optional[str] = None + nullable: bool = False + + def add_to_arg_parser(self, parser: argparse.ArgumentParser) -> None: + arg_names = [] + if self.short is not None: + arg_names.append('-' + self.short) + if self.long is not None: + arg_names.append('--' + self.long) + else: + arg_names.append('--' + self.name.replace('_', '-')) + + other = { + 'metavar': 'X', + } + + typ = self.parser + if typ is parse_bool: + other['nargs'] = '?' + other['const'] = 'true' + other['metavar'] = 'BOOL' + if self.nullable: + typ = make_nullable_parser(typ) + + parser.add_argument(*arg_names, type=typ, default=Ellipsis, help=self.help, **other) + + def args_to_obj(self, args: argparse.Namespace, obj: Any) -> None: + key = self.long.replace('-', '_') if self.long else self.name + val = getattr(args, key) + if val is not Ellipsis: + setattr(obj, self.name, val) + + +@dataclass +class AttrList: + attrs: List[Attr] + + def add_to_arg_parser(self, parser: argparse.ArgumentParser) -> None: + for a in self.attrs: + a.add_to_arg_parser(parser) + + def args_to_obj(self, args: argparse.Namespace, obj: Any) -> None: + for a in self.attrs: + a.args_to_obj(args, obj) + + +def parse_enum(e: Type[db.MOEnum], s: str) -> Any: + try: + return e.coerce(s) + except ValueError: + choices = ", ".join([x.name for x in e]) + raise argparse.ArgumentTypeError(f'Invalid value {s} (must be one of {choices})') + + +def parse_type(s: str) -> db.RoundType: + return parse_enum(db.RoundType, s) + + +def parse_time(s: str) -> Optional[datetime]: + return datetime.fromisoformat(s).astimezone() + + +def parse_bool(s: str) -> bool: + if s in ('true', '1', 'yes', 't', 'y'): + return True + if s in ('false', '0', 'no', 'f', 'n'): + return False + raise ValueError(f'Cannot parse {s} as Boolean value') + + +def parse_score_mode(s: str) -> db.RoundScoreMode: + return parse_enum(db.RoundScoreMode, s) + + +def parse_enroll_mode(s: str) -> db.RoundEnrollMode: + return parse_enum(db.RoundEnrollMode, s) + + +def make_nullable_parser(typ: Callable[[str], Any]) -> Any: + def parse(s: str) -> Any: + if s == '-' or s == "": + return None + else: + return typ(s) + + return parse + + +ROUND_ATTRS = AttrList([ + Attr('level', int, 'úroveň v hierarchii oblastí', short='l'), + Attr('name', str, 'název kola', short='n'), + Attr('round_type', parse_type, 'typ kola', long='type', short='t'), + # FIXME: state, last_state + # tasks_file nenastavujeme + Attr('ct_tasks_start', parse_time, 'začátek pro účastníky', nullable=True), + Attr('ct_submit_end', parse_time, 'konec odevzdávání pro účastníky'), + Attr('pr_tasks_start', parse_time, 'začátek pro dozor'), + Attr('pr_submit_end', parse_time, 'konec odevzdávání pro dozor'), + Attr('online_submit', parse_bool, 'povoleno elektronické odevzdávání'), + Attr('score_mode', parse_score_mode, 'režim výsledkové listiny'), + Attr('score_winner_limit', float, 'hranice bodů pro vítěze', nullable=True), + Attr('score_successful_limit', float, 'hranice bodů pro úspěšného řešitele', nullable=True), + Attr('score_advance_limit', float, 'hranice bodů pro postup', nullable=True), + Attr('points_step', float, 'bodovací krok', short='S'), + Attr('has_messages', parse_bool, 'povoleny zprávičky'), + Attr('enroll_mode', parse_enroll_mode, 'režim registrace'), + Attr('enroll_advert', str, 'popis v přihlášce'), + Attr('enroll_deadline', parse_time, 'deadline přihlašování', nullable=True), + Attr('switch_to_grading', parse_time, 'čas automatického přepnutí na opravování', nullable=True), + Attr('min_rec_grade', int, 'minimální doporučený ročník (1-12)', nullable=True), + Attr('max_rec_grade', int, 'maximální doporučený ročník (1-12)', nullable=True), + Attr('export_score_to_web', parse_bool, 'exportovat výsledkovky na web', long='publish-score'), +]) + + +CONTEST_ATTRS = AttrList([ + Attr('online_submit', parse_bool, 'povoleno elektronické odevzdávání'), + Attr('tex_hacks', str, 'hacky pro sazbu výsledkových listin'), +])