forked from ScoDoc/ScoDoc
gestion responsables de semestres
This commit is contained in:
parent
8f4b8ccdf6
commit
96788d588e
@ -38,6 +38,7 @@ import openpyxl.utils.datetime
|
|||||||
from openpyxl import Workbook, load_workbook
|
from openpyxl import Workbook, load_workbook
|
||||||
from openpyxl.cell import WriteOnlyCell
|
from openpyxl.cell import WriteOnlyCell
|
||||||
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||||
|
from openpyxl.comments import Comment
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import notesdb
|
from app.scodoc import notesdb
|
||||||
@ -257,7 +258,7 @@ class ScoExcelSheet:
|
|||||||
"""
|
"""
|
||||||
self.ws.column_dimensions[cle].hidden = value
|
self.ws.column_dimensions[cle].hidden = value
|
||||||
|
|
||||||
def make_cell(self, value: any = None, style=None):
|
def make_cell(self, value: any = None, style=None, comment=None):
|
||||||
"""Construit une cellule.
|
"""Construit une cellule.
|
||||||
value -- contenu de la cellule (texte ou numérique)
|
value -- contenu de la cellule (texte ou numérique)
|
||||||
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
||||||
@ -277,10 +278,20 @@ class ScoExcelSheet:
|
|||||||
cell.fill = style["fill"]
|
cell.fill = style["fill"]
|
||||||
if "alignment" in style:
|
if "alignment" in style:
|
||||||
cell.alignment = style["alignment"]
|
cell.alignment = style["alignment"]
|
||||||
|
if not comment is None:
|
||||||
|
cell.comment = Comment(comment, "scodoc")
|
||||||
|
cell.comment.width = 400
|
||||||
|
cell.comment.height = 150
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
def make_row(self, values: list, style=None):
|
def make_row(self, values: list, style=None, comments=None):
|
||||||
return [self.make_cell(value, style) for value in values]
|
# TODO make possible differents styles in a row
|
||||||
|
if comments is None:
|
||||||
|
comments = [None] * len(values)
|
||||||
|
return [
|
||||||
|
self.make_cell(value, style, comment)
|
||||||
|
for value, comment in zip(values, comments)
|
||||||
|
]
|
||||||
|
|
||||||
def append_single_cell_row(self, value: any, style=None):
|
def append_single_cell_row(self, value: any, style=None):
|
||||||
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
|
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
|
||||||
@ -367,7 +378,7 @@ class ScoExcelSheet:
|
|||||||
|
|
||||||
|
|
||||||
def excel_simple_table(
|
def excel_simple_table(
|
||||||
titles=None, lines=None, sheet_name=b"feuille", titles_styles=None
|
titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None
|
||||||
):
|
):
|
||||||
"""Export simple type 'CSV': 1ere ligne en gras, le reste tel quel"""
|
"""Export simple type 'CSV': 1ere ligne en gras, le reste tel quel"""
|
||||||
ws = ScoExcelSheet(sheet_name)
|
ws = ScoExcelSheet(sheet_name)
|
||||||
@ -378,9 +389,14 @@ def excel_simple_table(
|
|||||||
if titles_styles is None:
|
if titles_styles is None:
|
||||||
style = excel_make_style(bold=True)
|
style = excel_make_style(bold=True)
|
||||||
titles_styles = [style] * len(titles)
|
titles_styles = [style] * len(titles)
|
||||||
|
if comments is None:
|
||||||
|
comments = [None] * len(titles)
|
||||||
# ligne de titres
|
# ligne de titres
|
||||||
ws.append_row(
|
ws.append_row(
|
||||||
[ws.make_cell(it, style) for (it, style) in zip(titles, titles_styles)]
|
[
|
||||||
|
ws.make_cell(it, style, comment)
|
||||||
|
for (it, style, comment) in zip(titles, titles_styles, comments)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
default_style = excel_make_style()
|
default_style = excel_make_style()
|
||||||
text_style = excel_make_style(format_number="@")
|
text_style = excel_make_style(format_number="@")
|
||||||
|
@ -28,11 +28,13 @@
|
|||||||
"""Import d'utilisateurs via fichier Excel
|
"""Import d'utilisateurs via fichier Excel
|
||||||
"""
|
"""
|
||||||
import random, time
|
import random, time
|
||||||
|
import re
|
||||||
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
|
|
||||||
|
from app import db, Departement
|
||||||
from app.scodoc import sco_emails
|
from app.scodoc import sco_emails
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.notes_log import log
|
from app.scodoc.notes_log import log
|
||||||
@ -41,38 +43,61 @@ from app.scodoc import sco_excel
|
|||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
from flask_login import current_user
|
||||||
|
from app.auth.models import User, UserRole
|
||||||
|
|
||||||
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
|
TITLES = ("user_name", "nom", "prenom", "email", "roles", "dept")
|
||||||
|
COMMENTS = (
|
||||||
|
"""user_name:
|
||||||
|
Composé de lettres (minuscules ou majuscules), de chiffres ou du caractère _
|
||||||
|
""",
|
||||||
|
"""nom:
|
||||||
|
Maximum 64 caractères""",
|
||||||
|
"""prenom:
|
||||||
|
Maximum 64 caractères""",
|
||||||
|
"""email:
|
||||||
|
Maximum 120 caractères""",
|
||||||
|
"""roles:
|
||||||
|
un plusieurs rôles séparés par ','
|
||||||
|
chaque role est fait de 2 composantes séparées par _:
|
||||||
|
1. Le role (Ens, Secr ou Admin)
|
||||||
|
2. Le département (en majuscule)
|
||||||
|
Exemple: "Ens_RT,Admin_INFO"
|
||||||
|
""",
|
||||||
|
"""dept:
|
||||||
|
Le département d'appartenance du l'utillsateur. Laisser vide si l'utilisateur intervient dans plusieurs dépatements
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_excel_sample():
|
def generate_excel_sample():
|
||||||
"""generates an excel document suitable to import users"""
|
"""generates an excel document suitable to import users"""
|
||||||
style = sco_excel.excel_make_style(bold=True)
|
style = sco_excel.excel_make_style(bold=True)
|
||||||
titles = TITLES
|
titles = TITLES
|
||||||
titlesStyles = [style] * len(titles)
|
titles_styles = [style] * len(titles)
|
||||||
return sco_excel.excel_simple_table(
|
return sco_excel.excel_simple_table(
|
||||||
titles=titles, titlesStyles=titlesStyles, sheet_name="Utilisateurs ScoDoc"
|
titles=titles,
|
||||||
|
titles_styles=titles_styles,
|
||||||
|
sheet_name="Utilisateurs ScoDoc",
|
||||||
|
comments=COMMENTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def import_excel_file(datafile, REQUEST=None, context=None):
|
def import_excel_file(datafile):
|
||||||
"Create users from Excel file"
|
"Create users from Excel file"
|
||||||
authuser = REQUEST.AUTHENTICATED_USER
|
# Check current user privilege
|
||||||
auth_name = str(authuser)
|
auth_dept = current_user.dept
|
||||||
authuser_info = context._user_list(args={"user_name": auth_name})
|
auth_name = str(current_user)
|
||||||
zope_roles = authuser.getRolesInContext(context)
|
if not current_user.is_administrator():
|
||||||
if not authuser_info and not ("Manager" in zope_roles):
|
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
|
||||||
# not admin, and not in database
|
# Récupération des informations sur l'utilisateur courant
|
||||||
raise AccessDenied("invalid user (%s)" % auth_name)
|
|
||||||
if authuser_info:
|
|
||||||
auth_dept = authuser_info[0]["dept"]
|
|
||||||
else:
|
|
||||||
auth_dept = ""
|
|
||||||
log("sco_import_users.import_excel_file by %s" % auth_name)
|
log("sco_import_users.import_excel_file by %s" % auth_name)
|
||||||
|
|
||||||
exceldata = datafile.read()
|
exceldata = datafile.read()
|
||||||
if not exceldata:
|
if not exceldata:
|
||||||
raise ScoValueError("Ficher excel vide ou invalide")
|
raise ScoValueError("Ficher excel vide ou invalide")
|
||||||
_, data = sco_excel.Excel_to_list(exceldata)
|
_, data = sco_excel.excel_bytes_to_list(exceldata)
|
||||||
if not data: # probably a bug
|
if not data: # probably a bug
|
||||||
raise ScoException("import_excel_file: empty file !")
|
raise ScoException("import_excel_file: empty file !")
|
||||||
# 1- --- check title line
|
# 1- --- check title line
|
||||||
@ -99,10 +124,10 @@ def import_excel_file(datafile, REQUEST=None, context=None):
|
|||||||
d[fs[i]] = line[i]
|
d[fs[i]] = line[i]
|
||||||
U.append(d)
|
U.append(d)
|
||||||
|
|
||||||
return import_users(U, auth_dept=auth_dept, context=context)
|
return import_users(U, auth_dept=auth_dept)
|
||||||
|
|
||||||
|
|
||||||
def import_users(U, auth_dept="", context=None):
|
def import_users(users, auth_dept=""):
|
||||||
"""Import des utilisateurs:
|
"""Import des utilisateurs:
|
||||||
Pour chaque utilisateur à créer:
|
Pour chaque utilisateur à créer:
|
||||||
- vérifier données
|
- vérifier données
|
||||||
@ -112,9 +137,17 @@ def import_users(U, auth_dept="", context=None):
|
|||||||
|
|
||||||
En cas d'erreur: supprimer tous les utilisateurs que l'on vient de créer.
|
En cas d'erreur: supprimer tous les utilisateurs que l'on vient de créer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def append_msg(msg):
|
||||||
|
msg_list.append("Ligne %s : %s" % (line, msg))
|
||||||
|
|
||||||
created = [] # liste de uid créés
|
created = [] # liste de uid créés
|
||||||
|
msg_list = []
|
||||||
|
line = 1 # satr from excel line #2
|
||||||
|
ok = True
|
||||||
try:
|
try:
|
||||||
for u in U:
|
for u in users:
|
||||||
|
line = line + 1
|
||||||
ok, msg = sco_users.check_modif_user(
|
ok, msg = sco_users.check_modif_user(
|
||||||
0,
|
0,
|
||||||
user_name=u["user_name"],
|
user_name=u["user_name"],
|
||||||
@ -124,27 +157,60 @@ def import_users(U, auth_dept="", context=None):
|
|||||||
roles=u["roles"],
|
roles=u["roles"],
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
raise ScoValueError(
|
append_msg("identifiant '%s' %s" % (u["user_name"], msg))
|
||||||
"données invalides pour %s: %s" % (u["user_name"], msg)
|
# raise ScoValueError(
|
||||||
)
|
# "données invalides pour %s: %s" % (u["user_name"], msg)
|
||||||
|
# )
|
||||||
u["passwd"] = generate_password()
|
u["passwd"] = generate_password()
|
||||||
# si auth_dept, crée tous les utilisateurs dans ce departement
|
|
||||||
if auth_dept:
|
|
||||||
u["dept"] = auth_dept
|
|
||||||
#
|
#
|
||||||
context.create_user(u.copy())
|
# check identifiant
|
||||||
created.append(u["user_name"])
|
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", u["user_name"]):
|
||||||
except:
|
ok = False
|
||||||
log("import_users: exception: deleting %s" % str(created))
|
append_msg(
|
||||||
# delete created users
|
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
|
||||||
for user_name in created:
|
% u["user_name"]
|
||||||
context._user_delete(user_name)
|
)
|
||||||
raise # re-raise exception
|
elif len(u["user_name"]) > 64:
|
||||||
|
ok = False
|
||||||
|
append_msg(
|
||||||
|
"identifiant '%s' trop long (64 caractères)" % u["user_name"]
|
||||||
|
)
|
||||||
|
if len(u["nom"]) > 64:
|
||||||
|
ok = False
|
||||||
|
append_msg("nom '%s' trop long (64 caractères)" % u["nom"])
|
||||||
|
if len(u["prenom"]) > 64:
|
||||||
|
ok = False
|
||||||
|
append_msg("prenom '%s' trop long (64 caractères)" % u["prenom"])
|
||||||
|
if len(u["email"]) > 120:
|
||||||
|
ok = False
|
||||||
|
append_msg("email '%s' trop long (120 caractères)" % u["email"])
|
||||||
|
# check département
|
||||||
|
if u["dept"] != "":
|
||||||
|
dept = Departement.query.filter_by(acronym=u["dept"]).first()
|
||||||
|
if dept is None:
|
||||||
|
ok = False
|
||||||
|
append_msg("département '%s' inexistant" % u["dept"])
|
||||||
|
for role in u["roles"].split(","):
|
||||||
|
try:
|
||||||
|
_, _ = UserRole.role_dept_from_string(role)
|
||||||
|
except ScoValueError as value_error:
|
||||||
|
ok = False
|
||||||
|
append_msg("role : %s " % role)
|
||||||
|
# Création de l'utilisateur (via SQLAlchemy)
|
||||||
|
if ok:
|
||||||
|
user = User()
|
||||||
|
user.from_dict(u, new_user=True)
|
||||||
|
db.session.add(user)
|
||||||
|
created.append(u["user_name"])
|
||||||
|
db.session.commit()
|
||||||
|
except ScoValueError as value_error:
|
||||||
|
log("import_users: exception: abort create %s" % str(created))
|
||||||
|
db.session.rollback()
|
||||||
|
raise ScoValueError(msg) # re-raise exception
|
||||||
|
|
||||||
for u in U:
|
for user in users:
|
||||||
mail_password(u, context=context)
|
mail_password(user)
|
||||||
|
return ok, msg_list
|
||||||
return "ok"
|
|
||||||
|
|
||||||
|
|
||||||
# --------- Génération du mot de passe initial -----------
|
# --------- Génération du mot de passe initial -----------
|
||||||
@ -173,7 +239,11 @@ def mail_password(u, context=None, reset=False):
|
|||||||
if not u["email"]:
|
if not u["email"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
u["url"] = scu.ScoURL()
|
u[
|
||||||
|
"url"
|
||||||
|
] = (
|
||||||
|
scu.ScoURL()
|
||||||
|
) # TODO set auth page URL ? (shared by all departments) ../auth/login
|
||||||
|
|
||||||
txt = (
|
txt = (
|
||||||
"""
|
"""
|
||||||
@ -230,4 +300,4 @@ Pour plus d'informations sur ce logiciel, voir %s
|
|||||||
msg.epilogue = ""
|
msg.epilogue = ""
|
||||||
txt = MIMEText(txt, "plain", scu.SCO_ENCODING)
|
txt = MIMEText(txt, "plain", scu.SCO_ENCODING)
|
||||||
msg.attach(txt)
|
msg.attach(txt)
|
||||||
sco_emails.sendEmail(msg)
|
# sco_emails.sendEmail(msg) # TODO ScoDoc9 pending function
|
||||||
|
@ -94,11 +94,16 @@ def index_html(REQUEST, all_depts=False, with_inactives=False, format="html"):
|
|||||||
url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
|
url_for("users.create_user_form", scodoc_dept=g.scodoc_dept)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
H.append(
|
if current_user.is_administrator():
|
||||||
' <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
|
H.append(
|
||||||
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
|
' <a href="{}" class="stdlink">Importer des utilisateurs</a></p>'.format(
|
||||||
|
url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
H.append(
|
||||||
|
" Pour importer des utilisateurs en masse (via xlsx file) contactez votre administrateur scodoc."
|
||||||
)
|
)
|
||||||
)
|
|
||||||
if all_depts:
|
if all_depts:
|
||||||
checked = "checked"
|
checked = "checked"
|
||||||
else:
|
else:
|
||||||
|
@ -38,7 +38,8 @@ import re
|
|||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g
|
from flask import g, url_for
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
@ -54,7 +55,7 @@ from app.decorators import (
|
|||||||
permission_required,
|
permission_required,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header, sco_import_users, sco_excel
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
@ -62,6 +63,8 @@ from app.scodoc.notes_log import log
|
|||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
from app.scodoc.sco_permissions_check import can_handle_passwd
|
from app.scodoc.sco_permissions_check import can_handle_passwd
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||||
|
from app.scodoc.sco_excel import send_excel_file
|
||||||
|
from app.scodoc.sco_import_users import generate_excel_sample
|
||||||
from app.views import users_bp as bp
|
from app.views import users_bp as bp
|
||||||
|
|
||||||
|
|
||||||
@ -459,9 +462,87 @@ def create_user_form(REQUEST, user_name=None, edit=0):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/import_users_form")
|
@bp.route("/import_users_generate_excel_sample")
|
||||||
def import_users_form():
|
@scodoc
|
||||||
raise NotImplementedError()
|
@permission_required(Permission.ScoUsersAdmin)
|
||||||
|
@scodoc7func
|
||||||
|
def import_users_generate_excel_sample(REQUEST):
|
||||||
|
"une feuille excel pour importation utilisateurs"
|
||||||
|
data = sco_import_users.generate_excel_sample()
|
||||||
|
return sco_excel.send_excel_file(REQUEST, data, "ImportUtilisateurs")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/import_users_form", methods=["GET", "POST"])
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoUsersAdmin)
|
||||||
|
@scodoc7func
|
||||||
|
def import_users_form(REQUEST=None):
|
||||||
|
"""Import utilisateurs depuis feuille Excel"""
|
||||||
|
head = html_sco_header.sco_header(page_title="Import utilisateurs")
|
||||||
|
H = [
|
||||||
|
head,
|
||||||
|
"""<h2>Téléchargement d'une nouvelle liste d'utilisateurs</h2>
|
||||||
|
<p style="color: red">A utiliser pour importer de <b>nouveaux</b> utilisateurs (enseignants ou secrétaires)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
L'opération se déroule en deux étapes. Dans un premier temps,
|
||||||
|
vous téléchargez une feuille Excel type. Vous devez remplir
|
||||||
|
cette feuille, une ligne décrivant chaque utilisateur. Ensuite,
|
||||||
|
vous indiquez le nom de votre fichier dans la case "Fichier Excel"
|
||||||
|
ci-dessous, et cliquez sur "Télécharger" pour envoyer au serveur
|
||||||
|
votre liste.
|
||||||
|
</p>
|
||||||
|
""",
|
||||||
|
]
|
||||||
|
help = """<p class="help">
|
||||||
|
Lors de la creation des utilisateurs, les opérations suivantes sont effectuées:
|
||||||
|
</p>
|
||||||
|
<ol class="help">
|
||||||
|
<li>vérification des données;</li>
|
||||||
|
<li>génération d'un mot de passe alétoire pour chaque utilisateur;</li>
|
||||||
|
<li>création de chaque utilisateur;</li>
|
||||||
|
<li>envoi à chaque utilisateur de son <b>mot de passe initial par mail</b>.</li>
|
||||||
|
</ol>"""
|
||||||
|
H.append(
|
||||||
|
"""<ol><li><a class="stdlink" href="import_users_generate_excel_sample">
|
||||||
|
Obtenir la feuille excel à remplir</a></li><li>"""
|
||||||
|
)
|
||||||
|
F = html_sco_header.sco_footer()
|
||||||
|
tf = TrivialFormulator(
|
||||||
|
REQUEST.URL0,
|
||||||
|
REQUEST.form,
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"xlsfile",
|
||||||
|
{"title": "Fichier Excel:", "input_type": "file", "size": 40},
|
||||||
|
),
|
||||||
|
("formsemestre_id", {"input_type": "hidden"}),
|
||||||
|
),
|
||||||
|
submitlabel="Télécharger",
|
||||||
|
)
|
||||||
|
if tf[0] == 0:
|
||||||
|
return "\n".join(H) + tf[1] + "</li></ol>" + help + F
|
||||||
|
elif tf[0] == -1:
|
||||||
|
return flask.redirect(back_url)
|
||||||
|
else:
|
||||||
|
# IMPORT
|
||||||
|
ok, diag = sco_import_users.import_excel_file(tf[2]["xlsfile"])
|
||||||
|
# TODO Afficher la liste des messages
|
||||||
|
H = [html_sco_header.sco_header(page_title="Import utilisateurs")]
|
||||||
|
H.append("<ul>")
|
||||||
|
for d in diag:
|
||||||
|
H.append("<li>%s</li>" % d)
|
||||||
|
H.append("</ul>")
|
||||||
|
if ok:
|
||||||
|
dest = url_for("users.index_html", scodoc_dept=g.scodoc_dept)
|
||||||
|
H.append("<p>Ok, Import terminé !</p>")
|
||||||
|
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
|
||||||
|
else:
|
||||||
|
dest = url_for("users.import_users_form", scodoc_dept=g.scodoc_dept)
|
||||||
|
H.append("<p>Erreur, importation annulée !</p>")
|
||||||
|
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
|
||||||
|
return "\n".join(H) + html_sco_header.sco_footer()
|
||||||
|
return "\n".join(H) + help + F
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/user_info_page")
|
@bp.route("/user_info_page")
|
||||||
|
Loading…
Reference in New Issue
Block a user