forked from ScoDoc/ScoDoc
API: unification codes erreur HTTP + check group/partition names
This commit is contained in:
parent
e8accaf6a0
commit
c8801f6ee0
@ -9,6 +9,9 @@ from app.scodoc.sco_exceptions import ScoException
|
|||||||
api_bp = Blueprint("api", __name__)
|
api_bp = Blueprint("api", __name__)
|
||||||
api_web_bp = Blueprint("apiweb", __name__)
|
api_web_bp = Blueprint("apiweb", __name__)
|
||||||
|
|
||||||
|
# HTTP ERROR STATUS
|
||||||
|
API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
||||||
|
|
||||||
|
|
||||||
@api_bp.errorhandler(ScoException)
|
@api_bp.errorhandler(ScoException)
|
||||||
@api_bp.errorhandler(404)
|
@api_bp.errorhandler(404)
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
|
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Identite
|
from app.models import Identite
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
@ -48,7 +47,7 @@ def billets_absence_create():
|
|||||||
justified = data.get("justified", False)
|
justified = data.get("justified", False)
|
||||||
if None in (etudid, abs_begin, abs_end):
|
if None in (etudid, abs_begin, abs_end):
|
||||||
return json_error(
|
return json_error(
|
||||||
404, message="Paramètre manquant: etudid, abs_bein, abs_end requis"
|
404, message="Paramètre manquant: etudid, abs_begin, abs_end requis"
|
||||||
)
|
)
|
||||||
query = Identite.query.filter_by(etudid=etudid)
|
query = Identite.query.filter_by(etudid=etudid)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
|
@ -17,7 +17,7 @@ from flask_login import login_required
|
|||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.models import Departement, FormSemestre
|
from app.models import Departement, FormSemestre
|
||||||
@ -105,12 +105,12 @@ def departement_create():
|
|||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
acronym = str(data.get("acronym", ""))
|
acronym = str(data.get("acronym", ""))
|
||||||
if not acronym:
|
if not acronym:
|
||||||
return json_error(404, "missing acronym")
|
return json_error(API_CLIENT_ERROR, "missing acronym")
|
||||||
visible = bool(data.get("visible", True))
|
visible = bool(data.get("visible", True))
|
||||||
try:
|
try:
|
||||||
dept = departements.create_dept(acronym, visible=visible)
|
dept = departements.create_dept(acronym, visible=visible)
|
||||||
except ScoValueError as exc:
|
except ScoValueError as exc:
|
||||||
return json_error(404, exc.args[0] if exc.args else "")
|
return json_error(500, exc.args[0] if exc.args else "")
|
||||||
return jsonify(dept.to_dict())
|
return jsonify(dept.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ def departement_edit(acronym):
|
|||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
visible = bool(data.get("visible", None))
|
visible = bool(data.get("visible", None))
|
||||||
if visible is None:
|
if visible is None:
|
||||||
return json_error(404, "missing argument: visible")
|
return json_error(API_CLIENT_ERROR, "missing argument: visible")
|
||||||
visible = bool(visible)
|
visible = bool(visible)
|
||||||
dept.visible = visible
|
dept.visible = visible
|
||||||
db.session.add(dept)
|
db.session.add(dept)
|
||||||
|
@ -265,7 +265,7 @@ def bulletin(
|
|||||||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
|
||||||
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
||||||
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
||||||
return json_error(404, "formsemestre non trouve")
|
return json_error(404, "formsemestre inexistant")
|
||||||
app.set_sco_dept(dept.acronym)
|
app.set_sco_dept(dept.acronym)
|
||||||
|
|
||||||
if code_type == "nip":
|
if code_type == "nip":
|
||||||
|
@ -15,7 +15,6 @@ import app
|
|||||||
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
@ -11,7 +11,7 @@ from flask import g, jsonify, request
|
|||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
@ -113,7 +113,7 @@ def formsemestres_query():
|
|||||||
try:
|
try:
|
||||||
annee_scolaire_int = int(annee_scolaire)
|
annee_scolaire_int = int(annee_scolaire)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(404, "invalid annee_scolaire: not int")
|
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
||||||
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int)
|
||||||
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int)
|
||||||
formsemestres = formsemestres.filter(
|
formsemestres = formsemestres.filter(
|
||||||
@ -125,7 +125,7 @@ def formsemestres_query():
|
|||||||
try:
|
try:
|
||||||
dept_id = int(dept_id)
|
dept_id = int(dept_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return json_error(404, "invalid dept_id: not int")
|
return json_error(404, "invalid dept_id: integer expected")
|
||||||
formsemestres = formsemestres.filter_by(dept_id=dept_id)
|
formsemestres = formsemestres.filter_by(dept_id=dept_id)
|
||||||
if etape_apo is not None:
|
if etape_apo is not None:
|
||||||
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
||||||
@ -468,7 +468,7 @@ def formsemestre_resultat(formsemestre_id: int):
|
|||||||
"""
|
"""
|
||||||
format_spec = request.args.get("format", None)
|
format_spec = request.args.get("format", None)
|
||||||
if format_spec is not None and format_spec != "raw":
|
if format_spec is not None and format_spec != "raw":
|
||||||
return json_error(404, "invalid format specification")
|
return json_error(API_CLIENT_ERROR, "invalid format specification")
|
||||||
convert_values = format_spec != "raw"
|
convert_values = format_spec != "raw"
|
||||||
|
|
||||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
|
@ -8,15 +8,13 @@
|
|||||||
ScoDoc 9 API : jury WIP
|
ScoDoc 9 API : jury WIP
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import g, jsonify, request
|
from flask import jsonify
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_exceptions import ScoException
|
from app.scodoc.sco_exceptions import ScoException
|
||||||
from app.scodoc.sco_utils import json_error
|
|
||||||
from app.but import jury_but_results
|
from app.but import jury_but_results
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
@ -12,7 +12,7 @@ from flask_login import login_required
|
|||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db, log
|
from app import db, log
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||||
@ -138,7 +138,7 @@ def etud_in_group_query(group_id: int):
|
|||||||
"""Étudiants du groupe, filtrés par état"""
|
"""Étudiants du groupe, filtrés par état"""
|
||||||
etat = request.args.get("etat")
|
etat = request.args.get("etat")
|
||||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||||
return json_error(404, "etat: valeur invalide")
|
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||||
query = GroupDescr.query.filter_by(id=group_id)
|
query = GroupDescr.query.filter_by(id=group_id)
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
query = (
|
query = (
|
||||||
@ -281,9 +281,9 @@ def group_create(partition_id: int):
|
|||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
group_name = data.get("group_name")
|
group_name = data.get("group_name")
|
||||||
if group_name is None:
|
if group_name is None:
|
||||||
return json_error(404, "missing group name or invalid data format")
|
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||||
if not GroupDescr.check_name(partition, group_name):
|
if not GroupDescr.check_name(partition, group_name):
|
||||||
return json_error(404, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
group_name = group_name.strip()
|
group_name = group_name.strip()
|
||||||
|
|
||||||
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||||
@ -343,7 +343,7 @@ def group_edit(group_id: int):
|
|||||||
if group_name is not None:
|
if group_name is not None:
|
||||||
group_name = group_name.strip()
|
group_name = group_name.strip()
|
||||||
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||||
return json_error(404, "invalid group_name")
|
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||||
group.group_name = group_name
|
group.group_name = group_name
|
||||||
db.session.add(group)
|
db.session.add(group)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -381,14 +381,18 @@ def partition_create(formsemestre_id: int):
|
|||||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||||
partition_name = data.get("partition_name")
|
partition_name = data.get("partition_name")
|
||||||
if partition_name is None:
|
if partition_name is None:
|
||||||
return json_error(404, "missing partition_name or invalid data format")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, "missing partition_name or invalid data format"
|
||||||
|
)
|
||||||
if partition_name == scu.PARTITION_PARCOURS:
|
if partition_name == scu.PARTITION_PARCOURS:
|
||||||
return json_error(404, f"invalid partition_name {scu.PARTITION_PARCOURS}")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, f"invalid partition_name {scu.PARTITION_PARCOURS}"
|
||||||
|
)
|
||||||
if not Partition.check_name(formsemestre, partition_name):
|
if not Partition.check_name(formsemestre, partition_name):
|
||||||
return json_error(404, "invalid partition_name")
|
return json_error(API_CLIENT_ERROR, "invalid partition_name")
|
||||||
numero = data.get("numero", 0)
|
numero = data.get("numero", 0)
|
||||||
if not isinstance(numero, int):
|
if not isinstance(numero, int):
|
||||||
return json_error(404, "invalid type for numero")
|
return json_error(API_CLIENT_ERROR, "invalid type for numero")
|
||||||
args = {
|
args = {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre_id,
|
||||||
"partition_name": partition_name.strip(),
|
"partition_name": partition_name.strip(),
|
||||||
@ -399,7 +403,7 @@ def partition_create(formsemestre_id: int):
|
|||||||
boolean_field, False if boolean_field != "groups_editable" else True
|
boolean_field, False if boolean_field != "groups_editable" else True
|
||||||
)
|
)
|
||||||
if not isinstance(value, bool):
|
if not isinstance(value, bool):
|
||||||
return json_error(404, f"invalid type for {boolean_field}")
|
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
|
||||||
args[boolean_field] = value
|
args[boolean_field] = value
|
||||||
|
|
||||||
partition = Partition(**args)
|
partition = Partition(**args)
|
||||||
@ -433,7 +437,7 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
|||||||
isinstance(x, int) for x in partition_ids
|
isinstance(x, int) for x in partition_ids
|
||||||
):
|
):
|
||||||
return json_error(
|
return json_error(
|
||||||
404,
|
API_CLIENT_ERROR,
|
||||||
message="paramètre liste des partitions invalide",
|
message="paramètre liste des partitions invalide",
|
||||||
)
|
)
|
||||||
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
for p_id, numero in zip(partition_ids, range(len(partition_ids))):
|
||||||
@ -472,7 +476,7 @@ def partition_order_groups(partition_id: int):
|
|||||||
isinstance(x, int) for x in group_ids
|
isinstance(x, int) for x in group_ids
|
||||||
):
|
):
|
||||||
return json_error(
|
return json_error(
|
||||||
404,
|
API_CLIENT_ERROR,
|
||||||
message="paramètre liste de groupe invalide",
|
message="paramètre liste de groupe invalide",
|
||||||
)
|
)
|
||||||
for group_id, numero in zip(group_ids, range(len(group_ids))):
|
for group_id, numero in zip(group_ids, range(len(group_ids))):
|
||||||
@ -516,18 +520,20 @@ def partition_edit(partition_id: int):
|
|||||||
#
|
#
|
||||||
if partition_name is not None and partition_name != partition.partition_name:
|
if partition_name is not None and partition_name != partition.partition_name:
|
||||||
if partition.is_parcours():
|
if partition.is_parcours():
|
||||||
return json_error(404, f"can't rename {scu.PARTITION_PARCOURS}")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, f"can't rename {scu.PARTITION_PARCOURS}"
|
||||||
|
)
|
||||||
if not Partition.check_name(
|
if not Partition.check_name(
|
||||||
partition.formsemestre, partition_name, existing=True
|
partition.formsemestre, partition_name, existing=True
|
||||||
):
|
):
|
||||||
return json_error(404, "invalid partition_name")
|
return json_error(API_CLIENT_ERROR, "invalid partition_name")
|
||||||
partition.partition_name = partition_name.strip()
|
partition.partition_name = partition_name.strip()
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
numero = data.get("numero")
|
numero = data.get("numero")
|
||||||
if numero is not None and numero != partition.numero:
|
if numero is not None and numero != partition.numero:
|
||||||
if not isinstance(numero, int):
|
if not isinstance(numero, int):
|
||||||
return json_error(404, "invalid type for numero")
|
return json_error(API_CLIENT_ERROR, "invalid type for numero")
|
||||||
partition.numero = numero
|
partition.numero = numero
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
@ -535,9 +541,11 @@ def partition_edit(partition_id: int):
|
|||||||
value = data.get(boolean_field)
|
value = data.get(boolean_field)
|
||||||
if value is not None and value != getattr(partition, boolean_field):
|
if value is not None and value != getattr(partition, boolean_field):
|
||||||
if not isinstance(value, bool):
|
if not isinstance(value, bool):
|
||||||
return json_error(404, f"invalid type for {boolean_field}")
|
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
|
||||||
if boolean_field == "groups_editable" and partition.is_parcours():
|
if boolean_field == "groups_editable" and partition.is_parcours():
|
||||||
return json_error(404, f"can't change {scu.PARTITION_PARCOURS}")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}"
|
||||||
|
)
|
||||||
setattr(partition, boolean_field, value)
|
setattr(partition, boolean_field, value)
|
||||||
modified = True
|
modified = True
|
||||||
|
|
||||||
@ -571,7 +579,9 @@ def partition_delete(partition_id: int):
|
|||||||
if not partition.formsemestre.etat:
|
if not partition.formsemestre.etat:
|
||||||
return json_error(403, "formsemestre verrouillé")
|
return json_error(403, "formsemestre verrouillé")
|
||||||
if not partition.partition_name:
|
if not partition.partition_name:
|
||||||
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
return json_error(
|
||||||
|
API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut"
|
||||||
|
)
|
||||||
is_parcours = partition.is_parcours()
|
is_parcours = partition.is_parcours()
|
||||||
formsemestre: FormSemestre = partition.formsemestre
|
formsemestre: FormSemestre = partition.formsemestre
|
||||||
log(f"deleting partition {partition}")
|
log(f"deleting partition {partition}")
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
from app import db, log
|
from app import db
|
||||||
from app.api import api_bp as bp, api_web_bp
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.auth.models import User, Role, UserRole
|
from app.auth.models import User, Role, UserRole
|
||||||
from app.auth.models import is_valid_password
|
from app.auth.models import is_valid_password
|
||||||
@ -187,7 +187,7 @@ def user_password(uid: int):
|
|||||||
if not password:
|
if not password:
|
||||||
return json_error(404, "user_password: missing password")
|
return json_error(404, "user_password: missing password")
|
||||||
if not is_valid_password(password):
|
if not is_valid_password(password):
|
||||||
return json_error(400, "user_password: invalid password")
|
return json_error(API_CLIENT_ERROR, "user_password: invalid password")
|
||||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin)
|
||||||
if (None not in allowed_depts) and ((user.dept not in allowed_depts)):
|
if (None not in allowed_depts) and ((user.dept not in allowed_depts)):
|
||||||
return json_error(403, "user_password: departement non autorise")
|
return json_error(403, "user_password: departement non autorise")
|
||||||
|
@ -72,7 +72,7 @@ class Partition(db.Model):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(partition_name, str):
|
if not isinstance(partition_name, str):
|
||||||
return False
|
return False
|
||||||
if not len(partition_name.strip()) > 0:
|
if not (0 < len(partition_name.strip()) < SHORT_STR_LEN):
|
||||||
return False
|
return False
|
||||||
if (not existing) and (
|
if (not existing) and (
|
||||||
partition_name in [p.partition_name for p in formsemestre.partitions]
|
partition_name in [p.partition_name for p in formsemestre.partitions]
|
||||||
@ -147,7 +147,7 @@ class GroupDescr(db.Model):
|
|||||||
"""
|
"""
|
||||||
if not isinstance(group_name, str):
|
if not isinstance(group_name, str):
|
||||||
return False
|
return False
|
||||||
if not default and not len(group_name.strip()) > 0:
|
if not default and not (0 < len(group_name.strip()) < GROUPNAME_STR_LEN):
|
||||||
return False
|
return False
|
||||||
if (not existing) and (group_name in [g.group_name for g in partition.groups]):
|
if (not existing) and (group_name in [g.group_name for g in partition.groups]):
|
||||||
return False
|
return False
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.4.47"
|
SCOVERSION = "9.4.48"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user