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
Loading items

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
Loading items
Show changes

Commits on Source 90

......@@ -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):
......
......@@ -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),
......
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';
......@@ -56,3 +56,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
......
......@@ -9,7 +9,7 @@ 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
......@@ -136,7 +136,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 +250,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 +270,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 +318,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 +352,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 +498,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 +523,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 +535,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 +548,7 @@ role_type_names = {
RoleType.garant_skola: 'školní garant',
RoleType.dozor: 'dozor',
RoleType.opravovatel: 'opravovatel',
RoleType.pozorovatel: 'pozorovatel',
}
......@@ -543,12 +582,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()
......
......@@ -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,9 +2,12 @@
from enum import Enum, auto
from dataclasses import dataclass
from sqlalchemy import or_
from sqlalchemy.orm.query import Query
from typing import Set, List, Dict, Tuple, Optional
import mo
import mo.config as config
import mo.db as db
......@@ -23,7 +26,10 @@ class Right(Enum):
edit_points = auto() # Přidělovat body ve stavu "grading"
view_statement = auto() # Prohlížet zadání, pokud je dostupné pro dozor
add_users = auto()
edit_users = auto()
view_all_users = auto() # Prohlížet všechny uživatele
view_school_users = auto() # Prohlížet uživatele ze své školy (jen garant_skola)
edit_all_users = auto() # Editovat všechny účastníky
edit_school_users = auto() # Editovat uživatele ze své školy (jen garant_skola)
add_orgs = auto()
edit_orgs = auto()
......@@ -57,7 +63,8 @@ roles: List[Role] = [
Right.upload_submits,
Right.edit_points,
Right.add_users,
Right.edit_users,
Right.view_all_users,
Right.edit_all_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -75,7 +82,8 @@ roles: List[Role] = [
Right.edit_points,
Right.view_statement,
Right.add_users,
Right.edit_users,
Right.view_all_users,
Right.edit_all_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -93,7 +101,8 @@ roles: List[Role] = [
Right.edit_points,
Right.view_statement,
Right.add_users,
Right.edit_users,
Right.view_all_users,
Right.edit_all_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -101,8 +110,6 @@ roles: List[Role] = [
Role(
role=db.RoleType.garant_skola,
rights={
# FIXME: Až se pořádně rozjedou školní kola, asi chceme školním správcům omezit
# práva na editaci uživatelů. Viz issue #66.
Right.assign_rights,
Right.edit_place,
Right.manage_contest,
......@@ -112,7 +119,8 @@ roles: List[Role] = [
Right.edit_points,
Right.view_statement,
Right.add_users,
Right.edit_users,
Right.view_school_users,
Right.edit_school_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -136,6 +144,15 @@ roles: List[Role] = [
Right.view_statement,
},
),
Role(
role=db.RoleType.pozorovatel,
rights={
Right.view_contestants,
Right.view_submits,
Right.view_statement,
Right.view_all_users,
},
),
]
......@@ -191,12 +208,69 @@ class Rights:
# Práva na práci s uživateli
def can_view_user(self, user: db.User) -> bool:
if user.is_admin or user.is_org:
return True
elif self.have_right(Right.view_all_users):
return True
elif self.have_right(Right.view_school_users):
schools = self.get_user_schools(Right.view_school_users)
if schools:
q = db.get_session().query(db.User).filter_by(user_id=user.user_id)
q = self.restrict_user_query(q, schools)
if q.first():
return True
return False
else:
return False
def can_edit_user(self, user: db.User) -> bool:
if user.is_admin:
return self.user.is_admin # only admins can edit admins
elif user.is_org:
return self.have_right(Right.edit_orgs)
return self.have_right(Right.edit_users)
elif self.have_right(Right.edit_all_users):
return True
elif self.have_right(Right.edit_school_users):
schools = self.get_user_schools(Right.edit_school_users)
if schools:
q = db.get_session().query(db.User).filter_by(user_id=user.user_id)
q = self.restrict_user_query(q, schools)
if q.first():
return True
return False
else:
return False
def get_user_schools(self, right: Right) -> Set[db.Place]:
"""Vrátí seznam škol, kde má organizátor právo spravovat uživatele."""
places: Set[db.Place] = set()
for role in self.user_roles:
r = roles_by_type[role.role]
if right in r.rights:
places.add(role.place)
return places
def restrict_user_query(self, q: Query, schools: Set[db.Place]) -> Query:
"""Přidá k dotazu na hledání uživatelů podmínku na školy z dané množiny."""
sess = db.get_session()
school_ids = {s.place_id for s in schools}
q = q.filter(or_(
db.User.user_id.in_(
sess.query(db.Participant.user_id)
.filter(db.Participant.school.in_(school_ids))
.filter(db.Participant.year >= config.CURRENT_YEAR - 1)
),
db.User.user_id.in_(
sess.query(db.Participation.user_id)
.select_from(db.Participation)
.join(db.Contest)
.join(db.Round)
.filter(or_(db.Contest.place_id.in_(school_ids), db.Participation.place_id.in_(school_ids)))
.filter(db.Round.year == config.CURRENT_YEAR)
)
))
return q
class RoundRights(Rights):
......@@ -293,6 +367,7 @@ class Gatekeeper:
roles: List[db.UserRole]
parent_cache: Dict[int, List[db.Place]]
rights_cache: Dict[Tuple[Optional[int], Optional[int], Optional[str], Optional[int], Optional[db.RoleType]], Rights]
root_place: Optional[db.Place]
def __init__(self, user: db.User):
self.user = user
......@@ -300,6 +375,12 @@ class Gatekeeper:
assert user.is_org or user.is_admin
self.parent_cache = {}
self.rights_cache = {}
self.root_place = None
def get_root_place(self) -> db.Place:
if not self.root_place:
self.root_place = db.get_root_place()
return self.root_place
def get_ancestors(self, place: db.Place) -> List[db.Place]:
pid = place.place_id
......@@ -319,7 +400,7 @@ class Gatekeeper:
"""Posbírá role a práva, která se vztahují k danému místu (možno i tranzitivně) a soutěži.
Pokud place=None, omezení role na místo se nebere v úvahu.
Pokud year==None, vyhovují role s libovolným ročníkem; pokud year=0, vyhovují jen ty s neuvedeným ročníkem.
Podobně cat a seq.
Podobně cat a seq (u cat vyhoví jen ty s neuvedenou kategorií, když cat="").
Pokud min_role!=None, tak se uvažují jen role, které jsou v hierarchii alespoň na úrovni min_role."""
cache_key = (place.place_id if place is not None else None, year, cat, seq, min_role)
......@@ -360,7 +441,7 @@ class Gatekeeper:
elif for_place:
place = for_place
else:
place = db.get_root_place()
place = self.get_root_place()
rights = RoundRights()
rights.round = round
rights._clone_from(self.rights_for(
......
......@@ -52,10 +52,35 @@ def validate_and_find_school(kod: str) -> db.Place:
return place
def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool, reason: str) -> Tuple[db.User, bool]:
class CheckErrorOrgIsUser(mo.CheckError):
"""Při požadavku na orga nalezen uživatel nebo opačně."""
pass
def change_user_to_org(user, reason: str):
if (db.get_session().query(db.Participation, db.Contest, db.Round)
.select_from(db.Participation)
.join(db.Contest)
.join(db.Round)
.filter(db.Participation.user == user)
.filter(db.Round.year == config.CURRENT_YEAR)
.count()):
raise mo.CheckError("Převedení účastníka na organizátora se nezdařilo, protože se účastní aktuálního ročníku. Kontaktujte prosím správce.")
user.is_org = True
logger.info(f'{reason.title()}: Změna stavu uživatele user=#{user.user_id} na organizátora')
changes = db.get_object_changes(user)
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={'action': 'user-change-is-org', 'reason': reason, 'changes': changes},
)
def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool, reason: str, allow_change_user_to_org=False) -> Tuple[db.User, bool, bool]:
sess = db.get_session()
user = sess.query(db.User).filter_by(email=email).one_or_none()
is_new = user is None
is_change_user_to_org = False
if user is None: # HACK: Podmínku je nutné zapsat znovu místo užití is_new, jinak si s tím mypy neporadí
if not krestni or not prijmeni:
raise mo.CheckError('Osoba s daným emailem zatím neexistuje, je nutné uvést její jméno.')
......@@ -73,10 +98,14 @@ def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[s
raise mo.CheckError(f'Osoba již registrována s odlišným jménem {user.full_name()}')
if (user.is_admin or user.is_org) != is_org:
if is_org:
raise mo.CheckError('Nelze předefinovat účastníka na organizátora')
if allow_change_user_to_org:
change_user_to_org(user, reason)
is_change_user_to_org = True
else:
raise CheckErrorOrgIsUser('Nelze předefinovat účastníka na organizátora.')
else:
raise mo.CheckError('Nelze předefinovat organizátora na účastníka')
return user, is_new
raise mo.CheckError('Nelze předefinovat organizátora na účastníka.')
return user, is_new, is_change_user_to_org
def find_or_create_participant(user: db.User, year: int, school_id: Optional[int], birth_year: Optional[int], grade: Optional[str], reason: str) -> Tuple[db.Participant, bool]:
......@@ -184,7 +213,7 @@ def validate_password(passwd: str) -> bool:
def set_password(user: db.User, passwd: str, reset: bool = False):
salt = bcrypt.gensalt()
salt = bcrypt.gensalt(rounds=9)
hashed = bcrypt.hashpw(passwd.encode('utf-8'), salt)
user.password_hash = hashed.decode('us-ascii')
if reset:
......
......@@ -132,6 +132,7 @@ def need_login():
def init_request():
path = request.path
# XXX: Když celá aplikace běží v adresáři, request.path je relativní ke kořeni aplikace, ne celého webu
if path.startswith('/static/') or path.startswith('/assets/'):
# Pro statické soubory v development nasazení nepotřebujeme nastavovat
# nic dalšího (v ostrém nasazení je servíruje uwsgi)
......@@ -155,7 +156,6 @@ def init_request():
mo.util.current_log_user = user
# K některým podstromům je nutné mít speciální oprávnění
# XXX: Když celá aplikace běží v adresáři, request.path je relativní ke kořeni aplikace, ne celého webu
if path.startswith('/org/'):
if not user:
raise NeedLoginError()
......@@ -172,6 +172,12 @@ app.before_request(init_request)
### UWSGI glue ###
def collect_garbage() -> None:
mo.now = mo.util.get_now()
mo.jobs.process_jobs()
mo.users.expire_reg_requests()
# Čas od času se probudíme a spustíme garbage collector:
# - projdeme joby pro případ, že by se ztratil signál
# - expirujeme zastaralé joby
......@@ -179,10 +185,7 @@ app.before_request(init_request)
@app.cli.command('gc')
def gc():
"""Run garbage collector."""
mo.now = mo.util.get_now()
mo.jobs.process_jobs()
mo.users.expire_reg_requests()
collect_garbage()
try:
......@@ -193,7 +196,7 @@ try:
def mule_timer(signum):
# app.logger.debug('Mule: Timer tick')
with app.app_context():
garbage_collect()
collect_garbage()
# Obykle při vložení jobu dostaneme signál.
@signal(42, target='mule')
......@@ -218,6 +221,7 @@ except ImportError:
# Většina webu je v samostatných modulech
import mo.web.api
import mo.web.acct
import mo.web.doc
import mo.web.jinja
import mo.web.menu
import mo.web.misc
......
from flask import render_template
from mo.web import app
@app.route('/doc/')
def doc_index():
return render_template('doc_index.html')
@app.route('/doc/organizatori')
def doc_org():
return render_template('doc_org.html')
@app.route('/doc/gdpr')
def doc_gdpr():
return render_template('doc_gdpr.html')
@app.route('/doc/about')
def doc_about():
return render_template('doc_about.html')
@app.route('/doc/import')
def doc_import():
return render_template('doc_import.html')
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import flask_wtf.file
import wtforms
import wtforms.validators as validators
from typing import Optional
import mo
from mo.imports import Import, FileFormat
from mo.web import app
class ImportForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
fmt = wtforms.SelectField(
"Formát souboru",
choices=FileFormat.choices(), coerce=FileFormat.coerce,
default=FileFormat.cs_csv,
)
submit = wtforms.SubmitField('Importovat')
get_template = wtforms.SubmitField('Stáhnout šablonu')
def generic_import_page(form: ImportForm, imp: Optional[Import], redirect_url: str, template: str = 'org_generic_import.html', **kwargs):
# Případné další parametry (**kwargs) jsou předávány generování stránky
errs = []
warnings = []
if imp:
fmt = form.fmt.data
imp.fmt = fmt
if form.submit.data:
if form.file.data is not None:
file = form.file.data.stream
import_tmp = mo.util.link_to_dir(file.name, mo.util.data_dir('imports'), suffix='.csv')
if imp.run(import_tmp):
if imp.cnt_rows == 0:
flash('Soubor neobsahoval žádné řádky s daty', 'danger')
else:
flash(imp.get_after_import_message(), 'success')
return redirect(redirect_url)
else:
errs = imp.errors
warnings = imp.warnings
else:
flash('Vyberte si prosím soubor', 'danger')
elif form.get_template.data:
out = imp.get_template()
resp = app.make_response(out)
resp.content_type = fmt.get_content_type()
resp.headers.add('Content-Disposition', 'attachment; filename=OSMO-' + imp.template_basename + '.' + fmt.get_extension())
return resp
return render_template(
template,
errs=errs,
warnings=warnings,
form=form,
imp=imp,
**kwargs
)