Skip to content
Snippets Groups Projects
Commit cde02f69 authored by Martin Mareš's avatar Martin Mareš
Browse files

Merge branch 'mj/testing' into 'devel'

Testovací prostředí

See merge request !121
parents 23e88913 e88f66e5
Branches
No related tags found
1 merge request!121Testovací prostředí
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
pip install wheel pip install wheel
pip install -c constraints.txt --editable . pip install -c constraints.txt --editable .
# vytvořit mo/config.py podle etc/config.py.example # vytvořit mo/config.py podle etc/config.py.example
mkdir -p data/imports bin/init-data-dir
bin/flask run bin/flask run
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
# Inicializovat regiony v DB # Inicializovat regiony v DB
# Obstarat si extra/ruian/ a extra/schools/parsed/ z jiné instance (nebo je znovu stáhnout) # Obstarat si extra/ruian/ a extra/schools/parsed/ z jiné instance (nebo je znovu stáhnout)
. ../venv/bin/activate . ../venv/bin/activate
bin/test-init # případně podmnožinu bin/test-init mo_osmo_test data # případně podmnožinu
# Případně ručně otestovat, že uwsgi funguje # Případně ručně otestovat, že uwsgi funguje
# uwsgi --ini etc/osmo.ini # uwsgi --ini etc/osmo.ini
...@@ -73,3 +73,24 @@ ...@@ -73,3 +73,24 @@
## Mražení závislostí ## Mražení závislostí
pip freeze | grep -v '^osmo=' >constraints.txt pip freeze | grep -v '^osmo=' >constraints.txt
## Testovací prostředí
# Umíme automaticky založit testovací prostředí se soutěžemi, uživateli
# a jejich rolemi. Obvykle na to používáme samostatnou databázi a datový
# adresář.
# Do mo/config.py připsat:
SQLALCHEMY_DATABASE_URI = ... připojení k databázi mo_osmo_test
DATA_DIR = "data-test"
CURRENT_YEAR = 42
INSECURE_TEST_LOGIN = True
# Vytvoření prostředí
bin/test-init mo_osmo_test data-test
# Spuštění webových prohlížečů pro testovací uživatele
bin/test-browser garanti
# Přepnutí soutěží do konkrétního stavu
bin/test-state running
...@@ -8,6 +8,7 @@ from mo.util import die, init_standalone ...@@ -8,6 +8,7 @@ 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')
parser.add_argument(dest='round', type=str, metavar='YY-C-S[p]', help='kód kola') 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('-n', '--dry-run', default=False, action='store_true', help='pouze ukáže, co by bylo provedeno') parser.add_argument('-n', '--dry-run', default=False, action='store_true', help='pouze ukáže, co by bylo provedeno')
args = parser.parse_args() args = parser.parse_args()
...@@ -27,9 +28,18 @@ if round.state != db.RoundState.delegate: ...@@ -27,9 +28,18 @@ if round.state != db.RoundState.delegate:
else: else:
state = db.RoundState.preparing state = db.RoundState.preparing
region = None
if args.region is not None:
region = db.get_place_by_code(args.region)
if region is None:
die("Tato oblast neexistuje")
if round.is_subround(): if round.is_subround():
# Pokud je to podkolo, kopírujeme soutěže z hlavního kola # Pokud je to podkolo, kopírujeme soutěže z hlavního kola
for mc in sess.query(db.Contest).filter_by(round=round.master): q = sess.query(db.Contest).filter_by(round=round.master)
if region is not None:
q = q.filter_by(place=region.place_id)
for mc in q.all():
r = mc.place r = mc.place
print(f"{round.round_code()} pro místo {r.name}: zakládám (podsoutěž)") print(f"{round.round_code()} pro místo {r.name}: zakládám (podsoutěž)")
...@@ -44,8 +54,11 @@ if round.is_subround(): ...@@ -44,8 +54,11 @@ if round.is_subround():
}) })
else: else:
if region is None:
regions = sess.query(db.Place).filter_by(level=round.level).all() regions = sess.query(db.Place).filter_by(level=round.level).all()
assert regions, "Neexistují žádná místa dané úrovně" assert regions, "Neexistují žádná místa dané úrovně"
else:
regions = [region]
for r in regions: for r in regions:
if sess.query(db.Contest).filter_by(round=round, place=r).first(): if sess.query(db.Contest).filter_by(round=round, place=r).first():
......
...@@ -23,7 +23,8 @@ else ...@@ -23,7 +23,8 @@ else
fi fi
echo "Zakládám adresáře" echo "Zakládám adresáře"
mkdir -p $DEST/{log,var,data/{errors,imports,jobs,statements,submits,tmp,score}} mkdir -p $DEST/{log,var}
bin/init-data-dir $DEST/data
echo "Instaluji balíček" echo "Instaluji balíček"
pip install -c constraints.txt . pip install -c constraints.txt .
......
#!/bin/bash
set -e
DEST=${1:-.}
mkdir -p $DEST/{errors,imports,jobs,statements,submits,tmp,score}
#!/usr/bin/env python3
import argparse
from typing import Optional
import mo.db as db
import mo.users
import mo.util
from mo.util import die, init_standalone
parser = argparse.ArgumentParser(description='Přihlásí účastníka do soutěže')
parser.add_argument('--email', type=str, help='e-mailová adresa uživatele')
parser.add_argument('--uid', type=int, help='ID uživatele')
parser.add_argument('--round', type=str, required=True, metavar='YY-C-S[p]', help='kód kola')
parser.add_argument('--region', type=str, required=True, help='kód soutěžní oblasti')
parser.add_argument('--site', type=str, help='kód soutěžního místa (default: oblast)')
parser.add_argument('--state', type=str, help='stav účasti (default: active)')
args = parser.parse_args()
init_standalone()
sess = db.get_session()
if args.email and args.uid:
parser.error('--email a --uid nesmí být uvedeny současně')
elif args.email:
user = mo.users.user_by_email(args.email)
elif args.uid:
user = mo.users.user_by_uid(args.uid)
else:
parser.error('Je nutné vybrat uživatele pomocí --email nebo --uid')
if not user:
die("Tento uživatel neexistuje")
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.is_subround():
die("Nelze přihlašovat do sekundárních kol")
pant = sess.query(db.Participant).filter_by(user=user, year=round.year).first()
if pant is None:
die("Účastník není registrovaný v ročníku")
region = db.get_place_by_code(args.region)
if region is None:
die("Soutěžní oblast neexistuje")
site: Optional[db.Place]
if args.site is None:
site = region
else:
site = db.get_place_by_code(args.site)
if site is None:
die("Soutěžní místo neexistuje")
contest = sess.query(db.Contest).filter_by(round=round, place=region).first()
if contest is None:
die("V této oblasti se nesoutěží")
if sess.query(db.Participation).filter_by(user=user, contest=contest).first() is not None:
die('Uživatel se této soutěže již účastní')
if args.state is None:
state = db.PartState.active
else:
try:
state = db.PartState.coerce(args.state)
except ValueError:
die(f'Neznámý stav účasti {args.state}')
pion = db.Participation(
user=user,
contest=contest,
place=site,
state=state,
)
sess.add(pion)
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'add-to-contest', 'reason': 'script', 'new': db.row2dict(pion)},
)
sess.commit()
#!/usr/bin/env python3
import argparse
import mo.db as db
import mo.users
import mo.util
from mo.util import die, init_standalone
parser = argparse.ArgumentParser(description='Přihlásí účastníka do ročníku')
parser.add_argument('--email', type=str, help='e-mailová adresa uživatele')
parser.add_argument('--uid', type=int, help='ID uživatele')
parser.add_argument('--year', type=int, required=True, help='ročník MO')
parser.add_argument('--school', type=str, required=True, help='kód školy')
parser.add_argument('--grade', type=str, required=True, help='třída')
parser.add_argument('--birth-year', type=int, required=True, help='rok narození')
args = parser.parse_args()
init_standalone()
sess = db.get_session()
if args.email and args.uid:
parser.error('--email a --uid nesmí být uvedeny současně')
elif args.email:
user = mo.users.user_by_email(args.email)
elif args.uid:
user = mo.users.user_by_uid(args.uid)
else:
parser.error('Je nutné vybrat uživatele pomocí --email nebo --uid')
if not user:
die("Tento uživatel neexistuje")
if user.is_admin or user.is_org:
die("Tento uživatel je organizátor, nelze mu přidávat účast v ročníku")
school_place = db.get_place_by_code(args.school)
if school_place is None:
die("Tato škola neexistuje")
if school_place.type != db.PlaceType.school or school_place.school is None:
die('Toto místo není škola')
try:
grade = mo.users.normalize_grade(args.grade, school_place.school)
except mo.CheckError as e:
die(str(e))
if sess.query(db.Participant).filter_by(user=user, year=args.year).first() is not None:
die('Uživatel se tohoto ročníku již účastní')
pant = db.Participant(
user=user,
year=args.year,
school_place=school_place,
birth_year=args.birth_year,
grade=grade,
)
sess.add(pant)
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'create-participant', 'reason': 'script', 'new': db.row2dict(pant)},
)
sess.commit()
#!/bin/bash
set -e
if [ $# != 1 ] ; then
echo >&2 "Usage: $0 <user without @test>"
exit 1
fi
browser ()
{
local U
mkdir -p test-chrome
for U in "$@" ; do
google-chrome --no-default-browser-check --no-first-run --user-data-dir=test-chrome/$U http://localhost:5000/test-login/$U@test &
if [ $# != 1 ] ; then
sleep 1
fi
done
}
case $1 in
garanti) browser cg kg og
;;
skoly) browser sg-sutka sg-brno sg-gulz sg-gkj
;;
dozor) browser sd
;;
oprav) browser ko so
;;
ucast) browser u1 u2 u3 u4 u5
;;
*) browser $1
;;
esac
#!/bin/bash #!/bin/bash
set -e set -e
if [ $# != 1 ] ; then if [ $# != 2 -o "$1" = "--help" ] ; then
echo >&2 "Použití: $0 <jméno-db>" cat <<AMEN
Použití: $0 <jméno-databáze> <datový-adresář>
POZOR: Databáze i datový adresář jsou kompletně přepsané!!!
Obojí musí odpovídat nastavení v mo.config.
AMEN
exit 1 exit 1
fi fi
DB=$1 DB="$1"
DATA="$2"
T_GREEN="$(tput setaf 2)"
T_NORMAL="$(tput sgr0)"
PATH="./bin:$PATH"
progress ()
{
echo "${T_GREEN}>>> $1${T_NORMAL}"
}
run ()
{
echo "+ $@"
"$@"
}
init_data ()
{
progress "Inicializuji databázi"
psql "$DB" <db/drop-all.sql
psql "$DB" <db/db.ddl
progress "Inicializuji datový adresář"
rm -rf "$DATA"
run init-data-dir "$DATA"
progress "Zakládám regiony"
run init-regions
progress "Zakládám školy"
run init-schools
progress "Zkracuji školy"
run shorten-schools
}
init_codes ()
{
progress "Přiděluji školám kódy"
run test-school-code --red-izo 600039811 --code zs-sutka # ZŠ Na Šutce, P8
run test-school-code --red-izo 691008736 --code zs-brno # Scio ZŠ Brno
run test-school-code --red-izo 600005933 --code gulz # G U Libeň. zámku, P8
run test-school-code --red-izo 600013481 --code gkj # G tř. Kpt. Jaroše, Brno
}
init_users ()
{
progress "Zakládám uživatele"
run create-user admin@test Hlavní Administrátor --admin --passwd brum
run create-user cg@test Celostátní Garant --org --passwd brum
run add-role --email cg@test --role garant
run create-user kg@test Krajský Garant --org --passwd brum
run add-role --email kg@test --role garant_kraj --cat A --place A
run create-user og@test Okresní Garant --org --passwd brum
run add-role --email og@test --role garant_okres --cat Z --place P8
run create-user sg-sutka@test Garant 'ZŠ Šutka' --org --passwd brum
run add-role --email sg-sutka@test --role garant_skola --place zs-sutka
run create-user sg-brno@test Garant 'ZŠ Brno' --org --passwd brum
run add-role --email sg-brno@test --role garant_skola --place zs-brno
run create-user sg-gulz@test Garant 'G Libeň' --org --passwd brum
run add-role --email sg-gulz@test --role garant_skola --place gulz
run create-user sg-gkj@test Garant 'G Jaroše' --org --passwd brum
run add-role --email sg-gkj@test --role garant_skola --place gkj
run create-user ko@test Krajský Opravovatel --org --passwd brum
run add-role --email ko@test --role opravovatel --place A
run create-user so@test Školní Opravovatel --org --passwd brum
run add-role --email so@test --role opravovatel --cat Z --place zs-sutka
run create-user sd@test Školní Dozor --org --passwd brum
run add-role --email sd@test --role dozor --cat Z --place zs-sutka
run create-user u1@test První Účastník --passwd brum
run create-user u2@test Druhý Účastník --passwd brum
run create-user u3@test Třetí Účastník --passwd brum
run create-user u4@test Čtvrtý Účastník --passwd brum
run create-user u5@test Pátý Účastník --passwd brum
}
init_contests ()
{
progress "Zakládám soutěže"
run init-year 42
# init-year zakládá krajská a vyšší kola, zde doplňujeme nižší
run create-contests 42-A-1 --region gulz
run create-contests 42-A-1 --region gkj
run create-contests 42-A-2 --region gulz
run create-contests 42-A-2 --region gkj
run create-contests 42-Z7-1 --region zs-sutka
run create-contests 42-Z7-1 --region zs-brno
run create-contests 42-Z7-2 --region P8
}
init_tasks()
{
progress "Zakládám úlohy"
local R
for R in 42-A-{1,2,3,4a,4b} 42-Z7-{1,2} ; do
run create-tasks $R $(echo $R | cut -d- -f3) 4 --points 10
done
}
init_contestants ()
{
progress "Přihlašuji soutěžící"
run register-year --email u1@test --year 42 --school gulz --grade 3/4 --birth-year=2002
run register-contest --email u1@test --round 42-A-1 --region gulz
run register-contest --email u1@test --round 42-A-2 --region gulz
run register-contest --email u1@test --round 42-A-3 --region A
run register-contest --email u1@test --round 42-A-4a --region CZ
psql $DB <db/drop-all.sql run register-year --email u2@test --year 42 --school gkj --grade 4/4 --birth-year=2001
psql $DB <db/db.ddl run register-contest --email u2@test --round 42-A-1 --region gkj
run register-contest --email u2@test --round 42-A-2 --region gkj
# V krajském kole soutěží v jiném kraji, než kam patří
run register-contest --email u2@test --round 42-A-3 --region B --site A
run register-contest --email u2@test --round 42-A-4a --region CZ
bin/init-regions run register-year --email u3@test --year 42 --school zs-sutka --grade 7 --birth-year=2010
bin/init-schools run register-contest --email u3@test --round 42-Z7-1 --region zs-sutka
bin/shorten-schools # V okresním kole soutěží na své škole
run register-contest --email u3@test --round 42-Z7-2 --region P8 --site zs-sutka
bin/create-user mj@ucw.cz Martin Mareš --admin --passwd brum run register-year --email u4@test --year 42 --school zs-sutka --grade 7 --birth-year=2011
run register-contest --email u4@test --round 42-Z7-1 --region zs-sutka
run register-contest --email u4@test --round 42-Z7-2 --region P8
bin/create-user medved@ucw.cz Baltasis Lokys --org --passwd brum # Na zs-brno registrujeme jen do prvního kola, ať se dá testovat zakládání
bin/add-role --email medved@ucw.cz --role garant --cat P # soutěží a postupy.
bin/add-role --email medved@ucw.cz --role garant_kraj --cat A --place S run register-year --email u5@test --year 42 --school zs-brno --grade 7 --birth-year=2011
run register-contest --email u5@test --round 42-Z7-1 --region zs-brno
}
bin/create-round -y 70 -c P -s 1 -n 'Školní kolo' -l 1 init_states ()
bin/create-round -y 70 -c P -s 2 -n 'Krajské kolo' -l 1 {
bin/create-round -y 70 -c P -s 3 -n 'Ústřední kolo' -l 0 progress "Generuji PDF zadání"
echo '# Zadání úlohy' >"$DATA/tmp/zadani.md"
pandoc "$DATA/tmp/zadani.md" -o "$DATA/tmp/zadani.pdf"
bin/create-round -y 70 -c A -s 1 -n 'Školní kolo (domácí)' -l 1 progress "Nastavuji stav soutěží"
bin/create-round -y 70 -c A -s 2 -n 'Školní kolo (klauzurní)' -l 1 run test-state preparing --statement "$DATA/tmp/zadani.pdf"
bin/create-round -y 70 -c A -s 3 -n 'Krajské kolo' -l 1 }
bin/create-round -y 70 -c A -s 4 -n 'Ústřední kolo' -l 0
bin/create-contests 70-P-2 init_data
bin/create-contests 70-A-3 init_codes
init_users
init_contests
init_tasks
init_contestants
init_states
#!/usr/bin/env python3
import argparse
import mo.db as db
import mo.util
from mo.util import die
parser = argparse.ArgumentParser(description='Přidělí kód škole')
parser.add_argument('--red-izo', type=str, required=True, help='RED_IZO školy')
parser.add_argument('--code', type=str, required=True, help='kód k přidělení')
args = parser.parse_args()
mo.util.init_standalone()
sess = db.get_session()
schools = sess.query(db.School).filter_by(red_izo=args.red_izo).all()
if not schools:
die("Zadané RED_IZO nemá žádná škola")
if len(schools) > 1:
die("Zadané RED_IZO má více škol")
schools[0].place.code = args.code
sess.commit()
#!/usr/bin/env python3
import argparse
from datetime import timedelta
import os
import mo
import mo.db as db
from mo.db import RoundState
from mo.util import die, init_standalone
parser = argparse.ArgumentParser(
description='Přepne stav testovacích soutěží',
epilog="""Stavy (kromě standardních z RoundState):
running-pre běží, ale ještě nezačala
running-early běží, dozor už vidí zadání, účastníci ne
running běží, účastníci mohou odevzdávat
running-late běží, pro účastníky soutěž skončila, pro dozor ještě ne
running-post běží, soutěž pro všechny skončila
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(dest='state', type=str, help='požadovaný stav')
parser.add_argument('--statement', type=str, help='nahraje zadání')
args = parser.parse_args()
init_standalone()
sess = db.get_session()
if args.state in ['running-pre', 'running-early', 'running-late', 'running-post']:
state = RoundState.running
else:
try:
state = RoundState.coerce(args.state)
except ValueError:
die("Neznámý stav")
rounds = sess.query(db.Round).all()
for r in rounds:
if r.level > 0 and state not in [RoundState.preparing, RoundState.closed]:
r.state = RoundState.delegate
else:
r.state = state
for c in sess.query(db.Contest).filter_by(round=r).all():
c.state = state
def set_times(delta) -> None:
t = mo.now - timedelta(hours=delta)
r.pr_tasks_start = t + timedelta(hours=1)
r.ct_tasks_start = t + timedelta(hours=3)
r.ct_submit_end = t + timedelta(hours=5)
r.pr_submit_end = t + timedelta(hours=7)
if state == RoundState.preparing:
set_times(0)
elif state == RoundState.running:
delta = {
'running-pre': 0,
'running-early': 2,
'running': 4,
'running-late': 6,
'running-post': 8,
}
set_times(delta[args.state])
else:
set_times(10)
if args.statement is not None:
stmt_dir = f'{r.year}-{r.category}-{r.seq}'
full_dir = os.path.join(mo.util.data_dir('statements'), stmt_dir)
os.makedirs(full_dir, exist_ok=True)
full_name = mo.util.link_to_dir(args.statement, full_dir, suffix='.pdf')
r.tasks_file = os.path.join(stmt_dir, os.path.basename(full_name))
sess.commit()
-- CREATE ROLE mo_osmo LOGIN PASSWORD 'pass'; -- CREATE ROLE mo_osmo LOGIN PASSWORD 'pass';
-- CREATE DATABASE mo_osmo WITH OWNER=mo_osmo; -- CREATE DATABASE mo_osmo WITH OWNER=mo_osmo;
-- GRANT mo_osmo TO some_admin; -- GRANT mo_osmo TO some_admin;
-- CREATE EXTENSION unaccent;
SET ROLE mo_osmo; SET ROLE mo_osmo;
-- Funkce pro odakcentování textu pomocí extension unaccent. -- Funkce pro odakcentování textu pomocí extension unaccent.
-- Je immutable, takže se dá používat i v indexech. -- Je immutable, takže se dá používat i v indexech.
-- Zdroj: http://stackoverflow.com/questions/11005036/does-postgresql-support-accent-insensitive-collations -- Zdroj: http://stackoverflow.com/questions/11005036/does-postgresql-support-accent-insensitive-collations
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE OR REPLACE FUNCTION f_unaccent(text) CREATE OR REPLACE FUNCTION f_unaccent(text)
RETURNS text AS RETURNS text AS
$func$ $func$
...@@ -149,8 +149,7 @@ CREATE TABLE rounds ( ...@@ -149,8 +149,7 @@ CREATE TABLE rounds (
enroll_mode enroll_mode NOT NULL DEFAULT 'manual', -- režim přihlašování (pro vyšší kola vždy 'manual') 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 enroll_advert varchar(255) NOT NULL DEFAULT '', -- popis v přihlašovacím formuláři
export_score_to_mo_web boolean NOT NULL DEFAULT false, -- automaticky exportovat výsledkovou listinu na web MO export_score_to_mo_web boolean NOT NULL DEFAULT false, -- automaticky exportovat výsledkovou listinu na web MO
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); CREATE INDEX rounds_master_round_id_index ON rounds (master_round_id);
......
...@@ -73,3 +73,7 @@ CURRENT_YEAR = 71 ...@@ -73,3 +73,7 @@ CURRENT_YEAR = 71
MO_WEB_SERVER = 'http://localhost:5001' MO_WEB_SERVER = 'http://localhost:5001'
MO_WEB_LOGIN = 'matemaicky_hroch' MO_WEB_LOGIN = 'matemaicky_hroch'
MO_WEB_PASSWD = 'BrumBrum' MO_WEB_PASSWD = 'BrumBrum'
# Povolení loginu bez hesla pro testovací uživatele (s adresou končící na @test).
# Nezapínat mimo testovací prostředí! Bydlí na /test-login/<email>.
#INSECURE_TEST_LOGIN = True
...@@ -659,3 +659,21 @@ def confirm_reset(): ...@@ -659,3 +659,21 @@ def confirm_reset():
@app.errorhandler(werkzeug.exceptions.Forbidden) @app.errorhandler(werkzeug.exceptions.Forbidden)
def handle_forbidden(e): def handle_forbidden(e):
return render_template('forbidden.html'), 403 return render_template('forbidden.html'), 403
if getattr(config, 'INSECURE_TEST_LOGIN', False):
@app.route('/test-login/<email>')
def test_login(email: str):
if not email.endswith('@test'):
app.logger.error('Test login: Uživatel <%s> nekončí na @test', email)
raise werkzeug.exceptions.NotFound()
user = mo.users.user_by_email(email)
if not user:
app.logger.error('Test login: Neznámý uživatel <%s>', email)
raise werkzeug.exceptions.Forbidden()
app.logger.info('Test login: Přihlásil se uživatel #%s <%s>', user.user_id, email)
mo.users.login(user)
db.get_session().commit()
return login_and_redirect(user)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment