Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
Show changes
Commits on Source (113)
recursive-include mo/web/templates *
recursive-include mo/web/static *
recursive-include mo/tex *
......@@ -10,6 +10,7 @@ parser = argparse.ArgumentParser(description='Založí soutěžní kolo')
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('-C', '--code', type=str, help='kód kola (default: roven pořadí)')
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')
......@@ -33,6 +34,7 @@ rnd = db.Round(
year=args.year,
category=args.cat,
seq=args.seq,
code=args.code or str(args.seq),
part=args.part,
level=args.level,
name=args.name,
......
#!/usr/bin/env python3
import argparse
import mo.db as db
import mo.util
from mo.util import die, init_standalone
parser = argparse.ArgumentParser(description='Založí úlohy pro dané kolo')
parser.add_argument(dest='round', type=str, metavar='YY-C-S[p]', help='kód kola')
parser.add_argument(dest='count', type=int, help='počet úloh')
parser.add_argument('-p', '--points', type=int, default=None, help='maximální počet bodů')
args = parser.parse_args()
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")
round = mo.util.get_round_by_code(round_code)
if round is None:
die("Kolo s tímto kódem neexistuje!")
if round.state != db.RoundState.preparing:
die("Kolo musí být ve stavu 'připravuje se'")
for i in range(1, args.count + 1):
code = f'{round.category}-{round.code or round.seq}-{i}'
task = sess.query(db.Task).filter_by(round=round, code=code).one_or_none()
if task:
print(f'{code}: již existuje')
else:
task = db.Task(round=round, code=code, name=f'Úloha {i}', max_points=args.points)
sess.add(task)
print(f'{code}: zakládám')
sess.commit()
......@@ -29,14 +29,17 @@ class Row:
output = []
for user in sorted(orgs, key=lambda u: u.sort_key()):
o = Row(
def row() -> Row:
return Row(
jmeno=user.full_name(),
email=user.email,
last_login=(user.last_login_at.strftime('%Y-%m-%d') if user.last_login_at is not None else '-'),
)
if user.roles:
for r in user.roles:
for r in sorted(user.roles, key=lambda r: (r.role, r.category or "", r.year or -1, r.seq or -1, r.place_id or -1)):
o = row()
o.role = r.role
o.kod_souteze = f"{r.category or '*'}-{r.year or '*'}-{r.seq or '*'}"
p = r.place
......@@ -44,6 +47,7 @@ for user in sorted(orgs, key=lambda u: u.sort_key()):
o.misto = p.type_name() + " " + p.name
output.append(o)
else:
o = row()
o.role = '-'
output.append(o)
......
......@@ -5,7 +5,7 @@ import sys
from mo.csv import FileFormat
import mo.db as db
from mo.imports import create_import, ImportType
from mo.imports import PointsImport
import mo.users
import mo.util
from mo.util import die
......@@ -37,14 +37,13 @@ user = mo.users.user_by_email(args.user)
if user is None:
die(f"Uživatel {args.user} neexistuje")
imp = create_import(
imp = PointsImport(
user,
type=ImportType.points,
fmt=FileFormat.tsv,
round=round,
task=task,
allow_add_del=args.add_del,
)
imp.fmt = FileFormat.tsv
if args.import_file:
if not imp.run(args.import_file):
......
#!/usr/bin/env python3
import mo.jobs
import mo.util
from mo.util import die, init_standalone
import argparse
parser = argparse.ArgumentParser(description='Spustí joby ve frontě')
parser.add_argument('-j', '--job', type=int, metavar='ID', help='Spustí konkrétní job')
parser.add_argument('-r', '--retry', default=False, action='store_true', help='Znovu spustí dokončený job')
args = parser.parse_args()
mo.util.init_standalone()
init_standalone()
if args.job is None:
if args.retry:
die("Přepínač --retry lze použít jen s --job")
mo.jobs.process_jobs()
else:
tj = mo.jobs.TheJob(args.job)
if not tj.load():
die("Tento job neexistuje")
tj.run(retry=args.retry)
......@@ -9,21 +9,29 @@ Flask==1.1.2
Flask-Bootstrap==3.3.7.1
Flask-SQLAlchemy==2.4.4
Flask-WTF==0.14.3
importlib-metadata==4.6.0
itsdangerous==1.1.0
Jinja2==2.11.2
lxml==4.6.2
markdown==3.3.4
Markdown==3.3.4
MarkupSafe==1.1.1
packaging==21.0
pikepdf==2.3.0
Pillow==8.1.0
pkg-resources==0.0.0
psycopg2==2.8.6
pycparser==2.20
pyparsing==2.4.7
python-dateutil==2.8.1
python-poppler==0.2.2
pytz==2020.5
pyzbar==0.1.8
six==1.15.0
SQLAlchemy==1.3.22
typing-extensions==3.10.0.0
uwsgidecorators==1.1.0
visitor==0.1.3
webencodings==0.5.1
Werkzeug==1.0.1
WTForms==2.3.3
zipp==3.5.0
......@@ -27,7 +27,8 @@ CREATE TABLE users (
last_login_at timestamp with time zone DEFAULT NULL,
reset_at timestamp with time zone DEFAULT NULL, -- poslední reset/aktivace nebo žádost o ně
password_hash varchar(255) DEFAULT NULL, -- heš hesla (je-li nastaveno)
note text NOT NULL DEFAULT '' -- poznámka viditelná pro orgy
note text NOT NULL DEFAULT '', -- poznámka viditelná pro orgy
email_notify boolean NOT NULL DEFAULT true -- přeje si dostávat mailové notifikace
);
-- Hierarchie regionů a organizací
......@@ -112,9 +113,10 @@ CREATE TABLE rounds (
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.
seq int NOT NULL, -- pořadí kola v kategorii (od 1)
part int NOT NULL DEFAULT 0, -- část kola (nenulová u dělených kol)
level int NOT NULL, -- úroveň hierarchie míst
code varchar(255) NOT NULL, -- kód kola ("1", "S" apod.)
name varchar(255) NOT NULL, -- zobrazované jméno ("Krajské kolo" apod.)
state round_state NOT NULL DEFAULT 'preparing', -- stav kola
tasks_file varchar(255) DEFAULT NULL, -- jméno souboru se zadáním úloh
......@@ -129,7 +131,8 @@ CREATE TABLE rounds (
has_messages boolean NOT NULL DEFAULT false, -- má zprávičky
enroll_mode enroll_mode NOT NULL DEFAULT 'manual', -- režim přihlašování (pro vyšší kola vždy 'manual')
enroll_advert varchar(255) NOT NULL DEFAULT '', -- popis v přihlašovacím formuláři
UNIQUE (year, category, seq, part)
UNIQUE (year, category, seq, part),
UNIQUE (year, category, code, part)
);
CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id);
......@@ -179,13 +182,20 @@ CREATE TABLE participations (
CREATE INDEX participations_contest_id_index ON participations (contest_id, place_id);
-- Úloha
-- Úlohy
CREATE TYPE task_type AS ENUM (
'regular', -- obyčejná úloha
'cms' -- praktická úloha kategorie P odevzdávaná přes CMS
);
CREATE TABLE tasks (
task_id serial PRIMARY KEY,
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 numeric(5,1) DEFAULT NULL, -- maximální počet bodů, pokud je nastaven, nelze zadat více bodů
type task_type NOT NULL DEFAULT 'regular',
UNIQUE (round_id, code)
);
......@@ -250,11 +260,14 @@ CREATE TYPE role_type AS ENUM (
'garant_okres', -- okresní garant
'garant_skola', -- školní garant
'dozor', -- dozor na soutěži (může odevzdávat řešení za účastníky)
'opravovatel' -- opravovatel
'opravovatel', -- opravovatel
'pozorovatel' -- pozorovatel (má read-only práva ke všemu)
);
-- Uživatelům majícím is_org=true lze přidělit roli ke konkrétnímu regionu (včetně podregionů) a volitelně kategorii/kolu
-- HACK: Pokud category='Z', role platí pro všechny kategorie začínající na 'Z' (extra výjimka v mo.rights)
-- HACK: category='Z 'platí pro všechny kategorie začínající na 'Z'
-- category='S' platí pro kategorie 'A', 'B', 'C'
-- (ošetřeno v mo.db.UserRight)
CREATE TABLE user_roles (
user_role_id serial PRIMARY KEY,
user_id int NOT NULL REFERENCES users(user_id),
......@@ -295,10 +308,13 @@ CREATE INDEX log_type_id_index ON log (type, id);
CREATE TYPE job_type AS ENUM (
'download_submits',
'upload_feedback'
'upload_feedback',
'create_protocols',
'process_scans'
);
CREATE TYPE job_state AS ENUM (
'preparing',
'ready',
'running',
'done', -- Hotovo, out_json a out_file jsou platné
......@@ -394,3 +410,20 @@ CREATE VIEW region_task_stats AS
JOIN solutions s USING(user_id, task_id)
JOIN region_descendants rd ON rd.descendant = c.place_id
GROUP BY r.round_id, rd.region, t.task_id;
-- Stav zpracování scanů (vázaný na joby)
CREATE TABLE scan_pages (
job_id int NOT NULL REFERENCES jobs(job_id) ON DELETE CASCADE,
file_nr int NOT NULL, -- co to je za stránku (od 0)
page_nr int NOT NULL,
user_id int DEFAULT NULL REFERENCES users(user_id), -- přiřazení účastníkovi a úloze
task_id int DEFAULT NULL REFERENCES tasks(task_id),
seq_id int NOT NULL, -- pořadové číslo v rámci úlohy (od 0)
-- Pokud user_id i task_id jsou NULL, seq_id znamená:
-- -1 pro stránku vyžadující pozornost
-- -2 pro prázdnou stránku
-- -3 pro pokračovací stránku
-- -4 pro stránku, která nepatří do této soutěže
UNIQUE (job_id, file_nr, page_nr)
);
SET ROLE 'mo_osmo';
ALTER TYPE job_type ADD VALUE 'create_protocols';
ALTER TYPE job_type ADD VALUE 'process_scans';
ALTER TYPE job_state ADD VALUE 'preparing';
CREATE TABLE scan_pages (
job_id int NOT NULL REFERENCES jobs(job_id) ON DELETE CASCADE,
file_nr int NOT NULL, -- co to je za stránku
page_nr int NOT NULL,
user_id int DEFAULT NULL REFERENCES users(user_id), -- přiřazení účastníkovi a úloze
task_id int DEFAULT NULL REFERENCES tasks(task_id),
seq_id int NOT NULL, -- pořadové číslo v rámci úlohy
UNIQUE (job_id, file_nr, page_nr)
);
SET ROLE 'mo_osmo';
CREATE TYPE task_type AS ENUM (
'regular', -- obyčejná úloha
'cms' -- praktická úloha kategorie P odevzdávaná přes CMS
);
ALTER TABLE tasks ADD COLUMN type task_type NOT NULL DEFAULT 'regular';
SET ROLE 'mo_osmo';
ALTER TABLE ROUNDS ADD COLUMN
code varchar(255) NOT NULL DEFAULT '';
SET ROLE mo_osmo;
ALTER TABLE users ADD COLUMN
email_notify boolean NOT NULL DEFAULT true;
SET ROLE mo_osmo;
ALTER TABLE rounds ALTER COLUMN code DROP DEFAULT;
UPDATE rounds SET code=seq WHERE code='';
ALTER TABLE rounds ADD UNIQUE(year, category, code, part);
SET ROLE mo_osmo;
ALTER TYPE role_type ADD VALUE 'pozorovatel';
......@@ -48,6 +48,9 @@ GC_PERIOD = 60
# Za jak dlouho expiruje dokončená dávka [min]
JOB_EXPIRATION = 5
# Některé dávky (analýza scanů) mají delší expiraci [min]
JOB_EXPIRATION_LONG = 1440
# Kolik nejvýše dovolujeme registrací za minutu
REG_MAX_PER_MINUTE = 10
......@@ -56,3 +59,8 @@ REG_TOKEN_VALIDITY = 10
# Aktuální ročník MO
CURRENT_YEAR = 71
# Instance CMS, ve které žijí praktické programovací úlohy, a její SSO secret.
# Pokud se neuvede nebo je None, praktické úlohy nejde odevzdávat.
# CMS_ROOT = 'https://contest.kam.mff.cuni.cz/cms/'
# CMS_SSO_SECRET = 'BrumBrum'
[uwsgi]
strict = true
chdir = /akce/mo/osmo-test/
socket = var/osmo.sock
chmod-socket = 666
......@@ -12,9 +13,9 @@ log-date = %%Y-%%m-%%d %%H:%%M:%%S
logformat-strftime
log-format = %(ftime) %(addr) %(method) "%(uri)" %(proto) %(status) t=%(msecs) rxb=%(cl) txb=%(rsize) pid=%(pid) user=%(osmo_uid)
master = True
master = true
processes = 2
vacuum = True
vacuum = true
die-on-term = true
max-requests = 10000
......
......@@ -5,11 +5,12 @@ import datetime
import decimal
from enum import Enum as PythonEnum, auto
import locale
import os
import re
from sqlalchemy import \
Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, \
text, func, \
create_engine, inspect, select
create_engine, inspect, select, or_, and_
from sqlalchemy.engine import Engine
from sqlalchemy.orm import relationship, sessionmaker, Session, class_mapper, joinedload, aliased
from sqlalchemy.orm.attributes import get_history
......@@ -22,6 +23,7 @@ from sqlalchemy.sql.sqltypes import Numeric
from typing import Optional, List, Tuple
import mo
import mo.config as config
from mo.place_level import place_levels, PlaceLevel
from mo.util_format import timeformat_short, timedelta, time_and_timedelta
......@@ -136,7 +138,8 @@ class Place(Base):
return place_levels[self.level].in_name() + " " + name
def get_root_place():
def get_root_place() -> Place:
"""Obvykle voláme mo.rights.Gatekeeper.get_root_place(), kterékešuje."""
return get_session().query(Place).filter_by(parent=None).one()
......@@ -249,6 +252,7 @@ class Round(Base):
seq = Column(Integer, nullable=False)
part = Column(Integer, nullable=False)
level = Column(Integer, nullable=False)
code = Column(String(255), nullable=False)
name = Column(String(255), nullable=False)
state = Column(Enum(RoundState, name='round_state'), nullable=False, server_default=text("'preparing'::round_state"))
tasks_file = Column(String(255))
......@@ -268,7 +272,7 @@ class Round(Base):
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}"
return f"{self.year}-{self.category}-{self.code}"
def part_code(self):
return chr(ord('a') + self.part - 1) if self.part > 0 else ""
......@@ -316,6 +320,25 @@ class Round(Base):
return " ".join(times)
def find_master_round(year: Optional[int], category: Optional[str], code: str) -> Round:
if not year:
raise mo.CheckError('Neuveden ročník pro nalezení kola')
if not category:
raise mo.CheckError('Neuvedena kategorie pro nalezení kola')
r = (
get_session().query(Round)
.filter_by(year=year, category=category, code=code)
.filter(Round.master_round_id == Round.round_id)
.all()
)
if len(r) < 1:
raise mo.CheckError(f'Kolo {year}-{category}-{code} nenalezeno')
if len(r) > 1:
raise mo.CheckError(f'Kolo {year}-{category}-{code} nelze určit jednoznačně')
return r[0]
class User(Base):
__tablename__ = 'users'
......@@ -331,6 +354,7 @@ class User(Base):
reset_at = Column(DateTime(True))
password_hash = Column(String(255), server_default=text("NULL::character varying"))
note = Column(Text, nullable=False, server_default=text("''::text"))
email_notify = Column(Boolean, nullable=False, server_default=text("true"))
roles = relationship('UserRole', primaryjoin='UserRole.user_id == User.user_id')
participants = relationship('Participant', primaryjoin='Participant.user_id == User.user_id')
......@@ -476,6 +500,20 @@ class Participation(Base):
user = relationship('User')
class TaskType(MOEnum):
regular = auto()
cms = auto()
def friendly_name(self) -> str:
return task_type_names[self]
task_type_names = {
TaskType.regular: 'standardní',
TaskType.cms: 'programovací (CMS)',
}
class Task(Base):
__tablename__ = 'tasks'
__table_args__ = (
......@@ -487,6 +525,7 @@ class Task(Base):
code = Column(String(255), nullable=False)
name = Column(String(255), nullable=False)
max_points = Column(Numeric)
type = Column(Enum(TaskType, name='task_type'), nullable=False, default=TaskType.regular)
round = relationship('Round')
......@@ -498,6 +537,7 @@ class RoleType(MOEnum):
garant_skola = auto()
dozor = auto()
opravovatel = auto()
pozorovatel = auto()
def friendly_name(self) -> str:
return role_type_names[self]
......@@ -510,6 +550,7 @@ role_type_names = {
RoleType.garant_skola: 'školní garant',
RoleType.dozor: 'dozor',
RoleType.opravovatel: 'opravovatel',
RoleType.pozorovatel: 'pozorovatel',
}
......@@ -543,12 +584,29 @@ class UserRole(Base):
return " ".join(parts)
# XXX: Tatáž logika je v DB dotazu v org_index()
def applies_to(self, at: Optional[Place] = None, year: Optional[int] = None, cat: Optional[str] = None, seq: Optional[int] = None) -> bool:
return ((at is None or self.place_id == at.place_id)
and (self.year is None or year is None or self.year == year)
and (self.category is None or cat is None or self.category == cat or (self.category == 'Z' and cat.startswith('Z')))
and (self.category is None
or cat is None
or self.category == cat
or (self.category == 'Z' and cat.startswith('Z'))
or (self.category == 'S' and cat in "ABC"))
and (self.seq is None or seq is None or self.seq == seq))
def is_legal(self) -> bool:
# Některé role mají omezení na úroveň hierarchie.
level = self.place.level if self.place else -1
rt = self.role
if (rt == RoleType.garant and not level <= 0
or rt == RoleType.garant_kraj and not level == 1
or rt == RoleType.garant_okres and not level == 2
or rt == RoleType.garant_skola and not level >= 3):
return False
return True
class PaperType(MOEnum):
solution = auto()
......@@ -629,9 +687,12 @@ class Solution(Base):
class JobType(MOEnum):
download_submits = auto()
upload_feedback = auto()
create_protocols = auto()
process_scans = auto()
class JobState(MOEnum):
preparing = auto()
ready = auto()
running = auto()
done = auto()
......@@ -642,6 +703,7 @@ class JobState(MOEnum):
job_state_names = {
JobState.preparing: 'připravuje se',
JobState.ready: 'čeká na spuštění',
JobState.running: 'zpracovává se',
JobState.done: 'dokončena',
......@@ -668,6 +730,14 @@ class Job(Base):
user = relationship('User')
def dir_path(self) -> str:
"""Adresář se soubory příslušejícími k jobu."""
# Nepoužíváme mo.util.data_dir, abychom se vyhnuli cyklické závislosti modulů.
return os.path.join(config.DATA_DIR, 'jobs', str(self.job_id))
def file_path(self, name: str) -> str:
return os.path.join(self.dir_path(), name)
class Message(Base):
__tablename__ = 'messages'
......@@ -750,6 +820,29 @@ class RegionTaskStat(Base):
task = relationship('Task')
class ScanPage(Base):
__tablename__ = 'scan_pages'
job_id = Column(Integer, ForeignKey('jobs.job_id', ondelete='CASCADE'), primary_key=True, nullable=False)
file_nr = Column(Integer, primary_key=True, nullable=False)
page_nr = Column(Integer, primary_key=True, nullable=False)
user_id = Column(Integer, ForeignKey('users.user_id'))
task_id = Column(Integer, ForeignKey('tasks.task_id'))
seq_id = Column(Integer, nullable=False)
UniqueConstraint('job_id', 'file_nr', 'page_nr')
job = relationship('Job')
user = relationship('User')
task = relationship('Task')
# Speciální seq_id ve ScanPage
SCAN_PAGE_FIX = -1
SCAN_PAGE_EMPTY = -2
SCAN_PAGE_CONTINUE = -3
SCAN_PAGE_UFO = -4
_engine: Optional[Engine] = None
_session: Optional[Session] = None
flask_db: Any = None
......
......@@ -74,7 +74,7 @@ def confirm_url(type: str, token: str) -> str:
def contestant_list_url(contest: db.Contest, registered_only: bool) -> str:
url = config.WEB_ROOT + f'org/contest/c/{contest.contest_id}/ucastnici'
url = config.WEB_ROOT + f'org/contest/c/{contest.contest_id}/participants'
if registered_only:
url += '?participation_state=registered'
return url
......
This diff is collapsed.
......@@ -2,6 +2,7 @@
from datetime import timedelta
import os
import shutil
from sqlalchemy import or_
from typing import Optional, Dict, Callable, List
......@@ -19,20 +20,6 @@ def send_notify():
logger.debug('Job: Není komu poslat notifikaci')
def job_file_path(name: str) -> str:
return os.path.join(mo.util.data_dir('jobs'), name)
def job_file_size(name: Optional[str]) -> Optional[int]:
if name is None:
return None
try:
return os.path.getsize(job_file_path(name))
except OSError:
return -1
class TheJob:
"""Job z pohledu Pythonu."""
......@@ -40,6 +27,7 @@ class TheJob:
job_id: Optional[int]
gatekeeper: Optional[mo.rights.Gatekeeper]
errors: List[str]
expires_in_minutes: int
def __init__(self, job_id: Optional[int] = None):
"""Pokud chceme pracovat s existujícím jobem, zadáme jeho ID."""
......@@ -47,40 +35,51 @@ class TheJob:
self.errors = []
def load(self) -> db.Job:
if getattr(self, 'job', None) is None:
sess = db.get_session()
self.job = sess.query(db.Job).with_for_update().get(self.job_id)
return self.job
def create(self, type: db.JobType, for_user: db.User) -> db.Job:
self.job = db.Job(type=type, state=db.JobState.ready, user=for_user)
return self.job
def attach_file(self, tmp_name: str, suffix: str):
"""Vytvoří hardlink na daný pracovní soubor v adresáři jobů."""
self.job = db.Job(type=type, state=db.JobState.preparing, user=for_user)
full_name = mo.util.link_to_dir(tmp_name, mo.util.data_dir('jobs'), suffix=suffix)
name = os.path.basename(full_name)
logger.debug(f'Job: Příloha {tmp_name} -> {name}')
return name
def submit(self):
# Do DB přidáváme nehotový job, protože potřebujeme znát job_id pro založení adresáře
sess = db.get_session()
sess.add(self.job)
sess.flush()
self.job_id = self.job.job_id
logger.info(f'Job: Vytvořen job #{self.job_id} pro uživatele #{self.job.user_id}')
sess.commit()
job_dir = self.job.dir_path()
if os.path.exists(job_dir):
# Hypoteticky by se mohlo stát, že se recykluje job_id od jobu, jehož
# vytvoření selhalo před commitem. Zkusíme tedy smazat prázdný adresář.
os.rmdir(job_dir)
os.mkdir(job_dir)
return self.job
def attach_file(self, tmp_name: str, attachment_name: str) -> str:
"""Vytvoří hardlink na daný pracovní soubor v adresáři jobu."""
full_name = self.job.file_path(attachment_name)
os.link(tmp_name, full_name)
logger.debug(f'Job: Příloha {tmp_name} -> {full_name}')
return attachment_name
def submit(self):
self.job.state = db.JobState.ready
db.get_session().commit()
send_notify()
def _finish_remove(self):
sess = db.get_session()
job = self.job
if job.in_file is not None:
mo.util.unlink_if_exists(job_file_path(job.in_file))
if job.out_file is not None:
mo.util.unlink_if_exists(job_file_path(job.out_file))
job_dir = self.job.dir_path()
if os.path.exists(job_dir):
shutil.rmtree(job_dir)
sess.delete(job)
sess.commit()
......@@ -106,21 +105,42 @@ class TheJob:
logger.info(f'Job: >> {msg}')
self.errors.append(msg)
def run(self):
def _check_runnable(self, retry: bool) -> Optional[str]:
s = self.job.state
if s == db.JobState.ready:
return None
elif s == db.JobState.running:
# Může se stát, že ho mezitím začal vyřizovat jiný proces
return 'právě běží'
elif s in (db.JobState.done, db.JobState.failed):
return None if retry else 'je už hotový'
else:
return 'je v neznámém stavu'
def run(self, retry: bool = False):
sess = db.get_session()
if not self.load() or self.job.state != db.JobState.ready:
# Někdo ho mezitím smazal nebo vyřídil
logger.info(f'Job: Job #{self.job_id} vyřizuje někdo jiný')
if not self.load():
# Někdo ho mezitím smazal
logger.info(f'Job: Job #{self.job_id} neexistuje')
sess.rollback()
return
reject_reason = self._check_runnable(retry)
if reject_reason is not None:
logger.info(f'Job: Job #{self.job_id} {reject_reason}')
sess.rollback()
return
job = self.job
logger.info(f'Job: Spouštím job #{job.job_id} ({job.type}) uživatele #{job.user_id}')
job.state = db.JobState.running
job.finished_at = None
job.expires_at = None
sess.commit()
try:
self.gatekeeper = mo.rights.Gatekeeper(job.user)
self.expires_in_minutes = config.JOB_EXPIRATION
_handler_table[job.type](self)
if self.errors:
logger.info(f'Job: Neúspěšně dokončen job #{job.job_id} ({job.result})')
......@@ -137,7 +157,7 @@ class TheJob:
job.result = 'Interní chyba, informujte prosím správce systému.'
job.finished_at = mo.util.get_now()
job.expires_at = job.finished_at + timedelta(minutes=config.JOB_EXPIRATION)
job.expires_at = job.finished_at + timedelta(minutes=self.expires_in_minutes)
sess.commit()
......@@ -189,4 +209,5 @@ def job_handler(type: db.JobType):
# Moduly implementující jednotlivé typy jobů
import mo.jobs.protocols
import mo.jobs.submit