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'),
+])