Merge remote-tracking branch 'origin/master' into temp

# Conflicts:
#	app/scodoc/sco_placement.py
This commit is contained in:
Jean-Marie Place 2021-09-17 10:12:16 +02:00
commit 42ef9f795f
82 changed files with 861 additions and 457 deletions

View File

@ -148,9 +148,15 @@ Mémo pour développeurs: séquence re-création d'une base:
flask import-scodoc7-users flask import-scodoc7-users
flask import-scodoc7-dept STID SCOSTID flask import-scodoc7-dept STID SCOSTID
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape.
# Paquet debian 11 # Paquet debian 11
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou
upgrade de scodoc9).
La préparation d'une release se fait à l'aide du script La préparation d'une release se fait à l'aide du script
`tools/build_release.sh`. `tools/build_release.sh`.

View File

@ -2,6 +2,7 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
import os import os
import re
import socket import socket
import sys import sys
import time import time
@ -12,19 +13,19 @@ from logging.handlers import SMTPHandler, WatchedFileHandler
from flask import current_app, g, request from flask import current_app, g, request
from flask import Flask from flask import Flask
from flask import abort, has_request_context from flask import abort, has_request_context, jsonify
from flask import render_template from flask import render_template
from flask.logging import default_handler from flask.logging import default_handler
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate from flask_migrate import Migrate
from flask_login import LoginManager from flask_login import LoginManager, current_user
from flask_mail import Mail from flask_mail import Mail
from flask_bootstrap import Bootstrap from flask_bootstrap import Bootstrap
from flask_moment import Moment from flask_moment import Moment
from flask_caching import Cache from flask_caching import Cache
import sqlalchemy import sqlalchemy
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams
from config import DevConfig from config import DevConfig
import sco_version import sco_version
@ -32,7 +33,7 @@ db = SQLAlchemy()
migrate = Migrate(compare_type=True) migrate = Migrate(compare_type=True)
login = LoginManager() login = LoginManager()
login.login_view = "auth.login" login.login_view = "auth.login"
login.login_message = "Please log in to access this page." login.login_message = "Identifiez-vous pour accéder à cette page."
mail = Mail() mail = Mail()
bootstrap = Bootstrap() bootstrap = Bootstrap()
moment = Moment() moment = Moment()
@ -56,6 +57,12 @@ def internal_server_error(e):
return render_template("error_500.html", SCOVERSION=sco_version.SCOVERSION), 500 return render_template("error_500.html", SCOVERSION=sco_version.SCOVERSION), 500
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
def render_raw_html(template_filename: str, **args) -> str: def render_raw_html(template_filename: str, **args) -> str:
"""Load and render an HTML file _without_ using Flask """Load and render an HTML file _without_ using Flask
Necessary for 503 error mesage, when DB is down and Flask may be broken. Necessary for 503 error mesage, when DB is down and Flask may be broken.
@ -76,7 +83,7 @@ def postgresql_server_error(e):
return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503 return render_raw_html("error_503.html", SCOVERSION=sco_version.SCOVERSION), 503
class RequestFormatter(logging.Formatter): class LogRequestFormatter(logging.Formatter):
"""Ajoute URL et remote_addr for logging""" """Ajoute URL et remote_addr for logging"""
def format(self, record): def format(self, record):
@ -86,12 +93,64 @@ class RequestFormatter(logging.Formatter):
else: else:
record.url = None record.url = None
record.remote_addr = None record.remote_addr = None
record.sco_user = current_user
return super().format(record) return super().format(record)
class LogExceptionFormatter(logging.Formatter):
"""Formatteur pour les exceptions: ajoute détails"""
def format(self, record):
if has_request_context():
record.url = request.url
record.remote_addr = request.environ.get(
"HTTP_X_FORWARDED_FOR", request.remote_addr
)
record.http_referrer = request.referrer
record.http_method = request.method
if request.method == "GET":
record.http_params = str(request.args)
else:
record.http_params = "(post data not loggued)"
else:
record.url = None
record.remote_addr = None
record.http_referrer = None
record.http_method = None
record.http_params = None
record.sco_user = current_user
return super().format(record)
class ScoSMTPHandler(SMTPHandler):
def getSubject(self, record: logging.LogRecord) -> str:
stack_summary = traceback.extract_tb(record.exc_info[2])
frame_summary = stack_summary[-1]
subject = f"ScoExc({sco_version.SCOVERSION}): {record.exc_info[0].__name__} in {frame_summary.name} {frame_summary.filename}"
return subject
class ReverseProxied(object):
"""Adaptateur wsgi qui nous permet d'avoir toutes les URL calculées en https
sauf quand on est en dev.
La variable HTTP_X_FORWARDED_PROTO est positionnée par notre config nginx"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
scheme = environ.get("HTTP_X_FORWARDED_PROTO")
if scheme:
environ["wsgi.url_scheme"] = scheme # ou forcer à https ici ?
return self.app(environ, start_response)
def create_app(config_class=DevConfig): def create_app(config_class=DevConfig):
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
app.wsgi_app = ReverseProxied(app.wsgi_app)
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.DEBUG)
app.config.from_object(config_class) app.config.from_object(config_class)
@ -107,6 +166,7 @@ def create_app(config_class=DevConfig):
app.register_error_handler(ScoValueError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(500, internal_server_error) app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error) app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage)
from app.auth import bp as auth_bp from app.auth import bp as auth_bp
@ -132,9 +192,16 @@ def create_app(config_class=DevConfig):
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences" absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
) )
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
scodoc_exc_formatter = RequestFormatter( scodoc_log_formatter = LogRequestFormatter(
"[%(asctime)s] %(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s in %(module)s: %(message)s" "%(levelname)s: %(message)s"
)
scodoc_exc_formatter = LogExceptionFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s: %(message)s\n"
"Referrer: %(http_referrer)s\n"
"Method: %(http_method)s\n"
"Params: %(http_params)s\n"
) )
if not app.testing: if not app.testing:
if not app.debug: if not app.debug:
@ -150,11 +217,11 @@ def create_app(config_class=DevConfig):
if app.config["MAIL_USE_TLS"]: if app.config["MAIL_USE_TLS"]:
secure = () secure = ()
host_name = socket.gethostname() host_name = socket.gethostname()
mail_handler = SMTPHandler( mail_handler = ScoSMTPHandler(
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]), mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
fromaddr="no-reply@" + app.config["MAIL_SERVER"], fromaddr="no-reply@" + app.config["MAIL_SERVER"],
toaddrs=["exception@scodoc.org"], toaddrs=["exception@scodoc.org"],
subject="ScoDoc Exception from " + host_name, subject="ScoDoc Exception", # unused see ScoSMTPHandler
credentials=auth, credentials=auth,
secure=secure, secure=secure,
) )
@ -163,7 +230,7 @@ def create_app(config_class=DevConfig):
app.logger.addHandler(mail_handler) app.logger.addHandler(mail_handler)
else: else:
# Pour logs en DEV uniquement: # Pour logs en DEV uniquement:
default_handler.setFormatter(scodoc_exc_formatter) default_handler.setFormatter(scodoc_log_formatter)
# Config logs pour DEV et PRODUCTION # Config logs pour DEV et PRODUCTION
# Configuration des logs (actifs aussi en mode development) # Configuration des logs (actifs aussi en mode development)
@ -172,9 +239,17 @@ def create_app(config_class=DevConfig):
file_handler = WatchedFileHandler( file_handler = WatchedFileHandler(
app.config["SCODOC_LOG_FILE"], encoding="utf-8" app.config["SCODOC_LOG_FILE"], encoding="utf-8"
) )
file_handler.setFormatter(scodoc_exc_formatter) file_handler.setFormatter(scodoc_log_formatter)
file_handler.setLevel(logging.INFO) file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler) app.logger.addHandler(file_handler)
# Log pour les erreurs (exceptions) uniquement:
# usually /opt/scodoc-data/log/scodoc_exc.log
file_handler = WatchedFileHandler(
app.config["SCODOC_ERR_FILE"], encoding="utf-8"
)
file_handler.setFormatter(scodoc_exc_formatter)
file_handler.setLevel(logging.ERROR)
app.logger.addHandler(file_handler)
# app.logger.setLevel(logging.INFO) # app.logger.setLevel(logging.INFO)
app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup") app.logger.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup")
@ -353,4 +428,4 @@ from app.scodoc import sco_cache
# click.echo( # click.echo(
# "Warning: user database not initialized !\n (use: flask user-db-init)" # "Warning: user database not initialized !\n (use: flask user-db-init)"
# ) # )
# admin = None # admin = None

View File

@ -16,20 +16,20 @@ _l = _
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
user_name = StringField(_l("Username"), validators=[DataRequired()]) user_name = StringField(_l("Nom d'utilisateur"), validators=[DataRequired()])
password = PasswordField(_l("Password"), validators=[DataRequired()]) password = PasswordField(_l("Mot de passe"), validators=[DataRequired()])
remember_me = BooleanField(_l("Remember Me")) remember_me = BooleanField(_l("mémoriser la connexion"))
submit = SubmitField(_l("Sign In")) submit = SubmitField(_l("Suivant"))
class UserCreationForm(FlaskForm): class UserCreationForm(FlaskForm):
user_name = StringField(_l("Username"), validators=[DataRequired()]) user_name = StringField(_l("Nom d'utilisateur"), validators=[DataRequired()])
email = StringField(_l("Email"), validators=[DataRequired(), Email()]) email = StringField(_l("Email"), validators=[DataRequired(), Email()])
password = PasswordField(_l("Password"), validators=[DataRequired()]) password = PasswordField(_l("Mot de passe"), validators=[DataRequired()])
password2 = PasswordField( password2 = PasswordField(
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")] _l("Répéter"), validators=[DataRequired(), EqualTo("password")]
) )
submit = SubmitField(_l("Register")) submit = SubmitField(_l("Inscrire"))
def validate_user_name(self, user_name): def validate_user_name(self, user_name):
user = User.query.filter_by(user_name=user_name.data).first() user = User.query.filter_by(user_name=user_name.data).first()
@ -48,9 +48,9 @@ class ResetPasswordRequestForm(FlaskForm):
class ResetPasswordForm(FlaskForm): class ResetPasswordForm(FlaskForm):
password = PasswordField(_l("Password"), validators=[DataRequired()]) password = PasswordField(_l("Mot de passe"), validators=[DataRequired()])
password2 = PasswordField( password2 = PasswordField(
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")] _l("Répéter"), validators=[DataRequired(), EqualTo("password")]
) )
submit = SubmitField(_l("Request Password Reset")) submit = SubmitField(_l("Request Password Reset"))

View File

@ -38,7 +38,7 @@ def login():
user = User.query.filter_by(user_name=form.user_name.data).first() user = User.query.filter_by(user_name=form.user_name.data).first()
if user is None or not user.check_password(form.password.data): if user is None or not user.check_password(form.password.data):
current_app.logger.info("login: invalid (%s)", form.user_name.data) current_app.logger.info("login: invalid (%s)", form.user_name.data)
flash(_("Invalid user name or password")) flash(_("Nom ou mot de passe invalide"))
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
login_user(user, remember=form.remember_me.data) login_user(user, remember=form.remember_me.data)
current_app.logger.info("login: success (%s)", form.user_name.data) current_app.logger.info("login: success (%s)", form.user_name.data)
@ -95,7 +95,7 @@ def reset_password_request():
current_app.logger.info( current_app.logger.info(
"reset_password_request: for unkown user '{}'".format(form.email.data) "reset_password_request: for unkown user '{}'".format(form.email.data)
) )
flash(_("Check your email for the instructions to reset your password")) flash(_("Voir les instructions envoyées par mail"))
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
return render_template( return render_template(
"auth/reset_password_request.html", title=_("Reset Password"), form=form "auth/reset_password_request.html", title=_("Reset Password"), form=form

View File

@ -43,12 +43,14 @@ class ZRequest(object):
"Emulating Zope 2 REQUEST" "Emulating Zope 2 REQUEST"
def __init__(self): def __init__(self):
if current_app.config["DEBUG"]: # if current_app.config["DEBUG"]:
self.URL = request.base_url
self.BASE0 = request.url_root # le ReverseProxied se charge maintenant de mettre le bon protocole http ou https
else: self.URL = request.base_url
self.URL = request.base_url.replace("http://", "https://") self.BASE0 = request.url_root
self.BASE0 = request.url_root.replace("http://", "https://") # else:
# self.URL = request.base_url.replace("http://", "https://")
# self.BASE0 = request.url_root.replace("http://", "https://")
self.URL0 = self.URL self.URL0 = self.URL
# query_string is bytes: # query_string is bytes:
self.QUERY_STRING = request.query_string.decode("utf-8") self.QUERY_STRING = request.query_string.decode("utf-8")

View File

@ -41,6 +41,7 @@ class Identite(db.Model):
code_nip = db.Column(db.Text()) code_nip = db.Column(db.Text())
code_ine = db.Column(db.Text()) code_ine = db.Column(db.Text())
# Ancien id ScoDoc7 pour les migrations de bases anciennes # Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archive
scodoc7_id = db.Column(db.Text(), nullable=True) scodoc7_id = db.Column(db.Text(), nullable=True)

View File

@ -19,7 +19,7 @@ class FormSemestre(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
formsemestre_id = db.synonym("id") formsemestre_id = db.synonym("id")
# dept_id est aussi dans la formation, ajpouté ici pour # dept_id est aussi dans la formation, ajouté ici pour
# simplifier et accélérer les selects dans notesdb # simplifier et accélérer les selects dans notesdb
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
@ -41,6 +41,10 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column( bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
# Bloque le calcul des moyennes (générale et d'UE)
block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# semestres decales (pour gestion jurys): # semestres decales (pour gestion jurys):
gestion_semestrielle = db.Column( gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
@ -70,6 +74,7 @@ class FormSemestre(db.Model):
"NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre" "NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre"
) )
# Ancien id ScoDoc7 pour les migrations de bases anciennes # Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archive
scodoc7_id = db.Column(db.Text(), nullable=True) scodoc7_id = db.Column(db.Text(), nullable=True)
def __init__(self, **kwargs): def __init__(self, **kwargs):

View File

@ -8,6 +8,7 @@
v 1.3 (python3) v 1.3 (python3)
""" """
import html
def TrivialFormulator( def TrivialFormulator(
@ -722,7 +723,9 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
if str(descr["allowed_values"][i]) == str(self.values[field]): if str(descr["allowed_values"][i]) == str(self.values[field]):
R.append('<span class="tf-ro-value">%s</span>' % labels[i]) R.append('<span class="tf-ro-value">%s</span>' % labels[i])
elif input_type == "textarea": elif input_type == "textarea":
R.append('<div class="tf-ro-textarea">%s</div>' % self.values[field]) R.append(
'<div class="tf-ro-textarea">%s</div>' % html.escape(self.values[field])
)
elif input_type == "separator" or input_type == "hidden": elif input_type == "separator" or input_type == "hidden":
pass pass
elif input_type == "file": elif input_type == "file":

View File

@ -87,10 +87,6 @@ Problème de connexion (identifiant, mot de passe): <em>contacter votre responsa
) )
_TOP_LEVEL_CSS = """
<style type="text/css">
</style>"""
_HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?> _HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
@ -105,31 +101,30 @@ _HTML_BEGIN = """<?xml version="1.0" encoding="%(encoding)s"?>
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" /> <link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" /> <link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script> <script src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/sorttable.js"></script> <script src="/ScoDoc/static/libjs/sorttable.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script> <script src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript"> <script>
window.onload=function(){enableTooltips("gtrcontent")}; window.onload=function(){enableTooltips("gtrcontent")};
</script> </script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script> <script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script> <script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script> <script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script> <script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script> <script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" /> <link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script> <script src="/ScoDoc/static/js/scodoc.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/etud_info.js"></script> <script src="/ScoDoc/static/js/etud_info.js"></script>
""" """
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"): def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
H = [ H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING}, _HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
_TOP_LEVEL_CSS,
"""</head><body class="gtrcontent" id="gtrcontent">""", """</head><body class="gtrcontent" id="gtrcontent">""",
scu.CUSTOM_HTML_HEADER_CNX, scu.CUSTOM_HTML_HEADER_CNX,
] ]
@ -185,13 +180,10 @@ def sco_header(
init_jquery = True init_jquery = True
H = [ H = [
"""<?xml version="1.0" encoding="%(encoding)s"?> """<!DOCTYPE html><html lang="fr">
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<meta charset="utf-8"/>
<title>%(page_title)s</title> <title>%(page_title)s</title>
<meta http-equiv="Content-Type" content="text/html; charset=%(encoding)s" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="LANG" content="fr" /> <meta name="LANG" content="fr" />
<meta name="DESCRIPTION" content="ScoDoc" /> <meta name="DESCRIPTION" content="ScoDoc" />
@ -206,9 +198,7 @@ def sco_header(
) )
if init_google_maps: if init_google_maps:
# It may be necessary to add an API key: # It may be necessary to add an API key:
H.append( H.append('<script src="https://maps.google.com/maps/api/js"></script>')
'<script type="text/javascript" src="https://maps.google.com/maps/api/js"></script>'
)
# Feuilles de style additionnelles: # Feuilles de style additionnelles:
for cssstyle in cssstyles: for cssstyle in cssstyles:
@ -223,9 +213,9 @@ def sco_header(
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" /> <link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" /> <link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script> <script src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script> <script src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript"> <script>
window.onload=function(){enableTooltips("gtrcontent")}; window.onload=function(){enableTooltips("gtrcontent")};
var SCO_URL="%(ScoURL)s"; var SCO_URL="%(ScoURL)s";
@ -236,16 +226,14 @@ def sco_header(
# jQuery # jQuery
if init_jquery: if init_jquery:
H.append( H.append(
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/jQuery/jquery.js"></script> """<script src="/ScoDoc/static/jQuery/jquery.js"></script>
""" """
) )
H.append( H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>'
)
# qTip # qTip
if init_qtip: if init_qtip:
H.append( H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>' '<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>'
) )
H.append( H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />' '<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
@ -253,32 +241,25 @@ def sco_header(
if init_jquery_ui: if init_jquery_ui:
H.append( H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>' '<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
)
# H.append('<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery-ui/js/jquery-ui-i18n.js"></script>')
H.append(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/js/scodoc.js"></script>'
) )
# H.append('<script src="/ScoDoc/static/libjs/jquery-ui/js/jquery-ui-i18n.js"></script>')
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
if init_google_maps: if init_google_maps:
H.append( H.append(
'<script type="text/javascript" src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>' '<script src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
) )
if init_datatables: if init_datatables:
H.append( H.append(
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>' '<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
) )
H.append( H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
'<script type="text/javascript" src="/ScoDoc/static/DataTables/datatables.min.js"></script>'
)
# JS additionels # JS additionels
for js in javascripts: for js in javascripts:
H.append( H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/%s"></script>\n"""
% js
)
H.append( H.append(
"""<style type="text/css"> """<style>
.gtrcontent { .gtrcontent {
margin-left: %(margin_left)s; margin-left: %(margin_left)s;
height: 100%%; height: 100%%;
@ -290,7 +271,7 @@ def sco_header(
) )
# Scripts de la page: # Scripts de la page:
if scripts: if scripts:
H.append("""<script language="javascript" type="text/javascript">""") H.append("""<script>""")
for script in scripts: for script in scripts:
H.append(script) H.append(script)
H.append("""</script>""") H.append("""</script>""")

View File

@ -28,9 +28,8 @@
""" """
Génération de la "sidebar" (marge gauche des pages HTML) Génération de la "sidebar" (marge gauche des pages HTML)
""" """
from flask import url_for from flask import render_template, url_for
from flask import g from flask import g, request
from flask import request
from flask_login import current_user from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -152,11 +151,12 @@ def sidebar():
# Logo # Logo
H.append( H.append(
f"""<div class="logo-insidebar"> f"""<div class="logo-insidebar">
<div class="sidebar-bottom"><a href="{ url_for( 'scolar.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/> <div class="sidebar-bottom"><a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }" class="sidebar">À propos</a><br/>
<a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a> <a href="{ scu.SCO_USER_MANUAL }" target="_blank" class="sidebar">Aide</a>
</div></div> </div></div>
<div class="logo-logo"><a href= { url_for( 'scolar.about', scodoc_dept=g.scodoc_dept ) } <div class="logo-logo">
">{ scu.icontag("scologo_img", no_size=True) }</a> <a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
{ scu.icontag("scologo_img", no_size=True) }</a>
</div> </div>
</div> </div>
<!-- end of sidebar --> <!-- end of sidebar -->
@ -167,19 +167,7 @@ def sidebar():
def sidebar_dept(): def sidebar_dept():
"""Partie supérieure de la marge de gauche""" """Partie supérieure de la marge de gauche"""
H = [ return render_template(
f"""<h2 class="insidebar">Dépt. {sco_preferences.get_preference("DeptName")}</h2> "sidebar_dept.html",
<a href="{url_for("scodoc.index")}" class="sidebar">Accueil</a> <br/> """ prefs=sco_preferences.SemPreferences(),
] )
dept_intranet_url = sco_preferences.get_preference("DeptIntranetURL")
if dept_intranet_url:
H.append(
f"""<a href="{dept_intranet_url}" class="sidebar">{
sco_preferences.get_preference("DeptIntranetTitle")}</a> <br/>"""
)
# Entreprises pas encore supporté en ScoDoc8
# H.append(
# """<br/><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br/>"""
# % infos
# )
return "\n".join(H)

View File

@ -186,6 +186,8 @@ class NotesTable(object):
self.use_ue_coefs = sco_preferences.get_preference( self.use_ue_coefs = sco_preferences.get_preference(
"use_ue_coefs", formsemestre_id "use_ue_coefs", formsemestre_id
) )
# si vrai, bloque calcul des moy gen. et d'UE.:
self.block_moyennes = self.sem["block_moyennes"]
# Infos sur les etudiants # Infos sur les etudiants
self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
@ -738,6 +740,7 @@ class NotesTable(object):
block_computation = ( block_computation = (
self.inscrdict[etudid]["etat"] == "D" self.inscrdict[etudid]["etat"] == "D"
or self.inscrdict[etudid]["etat"] == DEF or self.inscrdict[etudid]["etat"] == DEF
or self.block_moyennes
) )
moy_ues = {} moy_ues = {}

View File

@ -324,15 +324,14 @@ def list_abs_in_range(etudid, debut, fin, matin=None, moduleimpl_id=None, cursor
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
""" """SELECT DISTINCT A.JOUR, A.MATIN
SELECT DISTINCT A.JOUR, A.MATIN FROM ABSENCES A
FROM ABSENCES A WHERE A.ETUDID = %(etudid)s
WHERE A.ETUDID = %(etudid)s AND A.ESTABS"""
AND A.ESTABS"""
+ ismatin + ismatin
+ modul + modul
+ """ + """
AND A.JOUR BETWEEN %(debut)s AND %(fin)s AND A.JOUR BETWEEN %(debut)s AND %(fin)s
""", """,
{ {
"etudid": etudid, "etudid": etudid,
@ -639,7 +638,10 @@ def add_absence(
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"insert into absences (etudid,jour,estabs,estjust,matin,description, moduleimpl_id) values (%(etudid)s, %(jour)s, TRUE, %(estjust)s, %(matin)s, %(description)s, %(moduleimpl_id)s )", """
INSERT into absences (etudid,jour,estabs,estjust,matin,description, moduleimpl_id)
VALUES (%(etudid)s, %(jour)s, true, %(estjust)s, %(matin)s, %(description)s, %(moduleimpl_id)s )
""",
vars(), vars(),
) )
logdb( logdb(

View File

@ -218,7 +218,10 @@ def user_nbdays_since_last_notif(email_addr, etudid):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""select * from absences_notifications where email = %(email_addr)s and etudid=%(etudid)s order by notification_date desc""", """SELECT * FROM absences_notifications
WHERE email = %(email_addr)s and etudid=%(etudid)s
ORDER BY notification_date DESC
""",
{"email_addr": email_addr, "etudid": etudid}, {"email_addr": email_addr, "etudid": etudid},
) )
res = cursor.dictfetchone() res = cursor.dictfetchone()

View File

@ -628,14 +628,18 @@ def AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id=None):
# supr les absences non justifiees # supr les absences non justifiees
for date in dates: for date in dates:
cursor.execute( cursor.execute(
"delete from absences where etudid=%(etudid)s and (not estjust) and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s", """DELETE FROM absences
WHERE etudid=%(etudid)s and (not estjust) and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s
""",
vars(), vars(),
) )
sco_abs.invalidate_abs_etud_date(etudid, date) sco_abs.invalidate_abs_etud_date(etudid, date)
# s'assure que les justificatifs ne sont pas "absents" # s'assure que les justificatifs ne sont pas "absents"
for date in dates: for date in dates:
cursor.execute( cursor.execute(
"update absences set estabs=FALSE where etudid=%(etudid)s and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s", """UPDATE absences SET estabs=FALSE
WHERE etudid=%(etudid)s AND jour=%(date)s AND moduleimpl_id=%(moduleimpl_id)s
""",
vars(), vars(),
) )
if dates: if dates:
@ -840,9 +844,9 @@ def ListeAbsEtud(
# Formats non HTML et demande d'une seule table: # Formats non HTML et demande d'une seule table:
if format != "html" and format != "text": if format != "html" and format != "text":
if absjust_only == 1: if absjust_only == 1:
return tab_absjust.make_page(format=format, REQUEST=REQUEST) return tab_absjust.make_page(format=format)
else: else:
return tab_absnonjust.make_page(format=format, REQUEST=REQUEST) return tab_absnonjust.make_page(format=format)
if format == "html": if format == "html":
# Mise en forme HTML: # Mise en forme HTML:

View File

@ -34,11 +34,11 @@
Les PV de jurys et documents associés sont stockées dans un sous-repertoire de la forme Les PV de jurys et documents associés sont stockées dans un sous-repertoire de la forme
<archivedir>/<dept>/<formsemestre_id>/<YYYY-MM-DD-HH-MM-SS> <archivedir>/<dept>/<formsemestre_id>/<YYYY-MM-DD-HH-MM-SS>
(formsemestre_id est ici FormSemestre.scodoc7_id ou à défaut FormSemestre.id) (formsemestre_id est ici FormSemestre.id)
Les documents liés à l'étudiant sont dans Les documents liés à l'étudiant sont dans
<archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS> <archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
(etudid est ici soit Identite.scodoc7id, soit à défaut Identite.id) (etudid est ici Identite.id)
Les maquettes Apogée pour l'export des notes sont dans Les maquettes Apogée pour l'export des notes sont dans
<archivedir>/apo_csv/<dept>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv <archivedir>/apo_csv/<dept>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
@ -47,12 +47,13 @@
qui est une description (humaine, format libre) de l'archive. qui est une description (humaine, format libre) de l'archive.
""" """
import os
import time
import datetime import datetime
import glob
import mimetypes
import os
import re import re
import shutil import shutil
import glob import time
import flask import flask
from flask import g from flask import g
@ -244,31 +245,15 @@ class BaseArchiver(object):
log("reading archive file %s" % fname) log("reading archive file %s" % fname)
return open(fname, "rb").read() return open(fname, "rb").read()
def get_archived_file(self, REQUEST, oid, archive_name, filename): def get_archived_file(self, oid, archive_name, filename):
"""Recupere donnees du fichier indiqué et envoie au client""" """Recupere donnees du fichier indiqué et envoie au client"""
# XXX très incomplet: devrait inférer et assigner un type MIME
archive_id = self.get_id_from_name(oid, archive_name) archive_id = self.get_id_from_name(oid, archive_name)
data = self.get(archive_id, filename) data = self.get(archive_id, filename)
ext = os.path.splitext(filename.lower())[1] mime = mimetypes.guess_type(filename)[0]
if ext == ".html" or ext == ".htm": if mime is None:
return data mime = "application/octet-stream"
elif ext == ".xml":
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) return scu.send_file(data, filename, mime=mime)
return data
elif ext == ".xls":
return sco_excel.send_excel_file(
REQUEST, data, filename, mime=scu.XLS_MIMETYPE
)
elif ext == ".xlsx":
return sco_excel.send_excel_file(
REQUEST, data, filename, mime=scu.XLSX_MIMETYPE
)
elif ext == ".csv":
return scu.sendCSVFile(REQUEST, data, filename)
elif ext == ".pdf":
return scu.sendPDFFile(REQUEST, data, filename)
REQUEST.RESPONSE.setHeader("content-type", "application/octet-stream")
return data # should set mimetype for known files like images
class SemsArchiver(BaseArchiver): class SemsArchiver(BaseArchiver):
@ -305,7 +290,7 @@ def do_formsemestre_archive(
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id sem_archive_id = formsemestre_id
archive_id = PVArchive.create_obj_archive(sem_archive_id, description) archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
@ -519,7 +504,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
def formsemestre_list_archives(REQUEST, formsemestre_id): def formsemestre_list_archives(REQUEST, formsemestre_id):
"""Page listing archives""" """Page listing archives"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id sem_archive_id = formsemestre_id
L = [] L = []
for archive_id in PVArchive.list_obj_archives(sem_archive_id): for archive_id in PVArchive.list_obj_archives(sem_archive_id):
a = { a = {
@ -559,11 +544,11 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
def formsemestre_get_archived_file(REQUEST, formsemestre_id, archive_name, filename): def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client.""" """Send file to client."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id sem_archive_id = formsemestre_id
return PVArchive.get_archived_file(REQUEST, sem_archive_id, archive_name, filename) return PVArchive.get_archived_file(sem_archive_id, archive_name, filename)
def formsemestre_delete_archive( def formsemestre_delete_archive(
@ -575,7 +560,7 @@ def formsemestre_delete_archive(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER) "opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
) )
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id sem_archive_id = formsemestre_id
archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name) archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name)
dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id) dest_url = "formsemestre_list_archives?formsemestre_id=%s" % (formsemestre_id)

View File

@ -65,7 +65,7 @@ def etud_list_archives_html(REQUEST, etudid):
if not etuds: if not etuds:
raise ScoValueError("étudiant inexistant") raise ScoValueError("étudiant inexistant")
etud = etuds[0] etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etudid etud_archive_id = etudid
L = [] L = []
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
a = { a = {
@ -118,7 +118,7 @@ def add_archives_info_to_etud_list(etuds):
""" """
for etud in etuds: for etud in etuds:
l = [] l = []
etud_archive_id = etud["scodoc7_id"] or etud["etudid"] etud_archive_id = etud["etudid"]
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
l.append( l.append(
"%s (%s)" "%s (%s)"
@ -181,7 +181,7 @@ def etud_upload_file_form(REQUEST, etudid):
data = tf[2]["datafile"].read() data = tf[2]["datafile"].read()
descr = tf[2]["description"] descr = tf[2]["description"]
filename = tf[2]["datafile"].filename filename = tf[2]["datafile"].filename
etud_archive_id = etud["scodoc7_id"] or etud["etudid"] etud_archive_id = etud["etudid"]
_store_etud_file_to_new_archive( _store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=descr etud_archive_id, data, filename, description=descr
) )
@ -210,7 +210,7 @@ def etud_delete_archive(REQUEST, etudid, archive_name, dialog_confirmed=False):
if not etuds: if not etuds:
raise ScoValueError("étudiant inexistant") raise ScoValueError("étudiant inexistant")
etud = etuds[0] etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etud["etudid"] etud_archive_id = etud["etudid"]
archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name) archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
@ -242,16 +242,14 @@ def etud_delete_archive(REQUEST, etudid, archive_name, dialog_confirmed=False):
) )
def etud_get_archived_file(REQUEST, etudid, archive_name, filename): def etud_get_archived_file(etudid, archive_name, filename):
"""Send file to client.""" """Send file to client."""
etuds = sco_etud.get_etud_info(filled=True) etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not etuds: if not etuds:
raise ScoValueError("étudiant inexistant") raise ScoValueError("étudiant inexistant")
etud = etuds[0] etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etud["etudid"] etud_archive_id = etud["etudid"]
return EtudsArchive.get_archived_file( return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename)
REQUEST, etud_archive_id, archive_name, filename
)
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants) # --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)

View File

@ -35,9 +35,8 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.header import Header from email.header import Header
from reportlab.lib.colors import Color from reportlab.lib.colors import Color
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import urllib
from flask import g from flask import g
from flask import url_for from flask import url_for
@ -444,7 +443,10 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
if mod["mod_moy_txt"][:2] == "NA": if mod["mod_moy_txt"][:2] == "NA":
mod["mod_moy_txt"] = "-" mod["mod_moy_txt"] = "-"
if is_malus: if is_malus:
if mod_moy > 0: if isinstance(mod_moy, str):
mod["mod_moy_txt"] = "-"
mod["mod_coef_txt"] = "-"
elif mod_moy > 0:
mod["mod_moy_txt"] = scu.fmt_note(mod_moy) mod["mod_moy_txt"] = scu.fmt_note(mod_moy)
mod["mod_coef_txt"] = "Malus" mod["mod_coef_txt"] = "Malus"
elif mod_moy < 0: elif mod_moy < 0:
@ -1061,7 +1063,7 @@ def _formsemestre_bulletinetud_header_html(
# Menu # Menu
endpoint = "notes.formsemestre_bulletinetud" endpoint = "notes.formsemestre_bulletinetud"
url = REQUEST.URL0 url = REQUEST.URL0
qurl = six.moves.urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING) qurl = urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING)
menuBul = [ menuBul = [
{ {

View File

@ -100,7 +100,7 @@ class ScoDocCache:
log("Error: cache set failed !") log("Error: cache set failed !")
except: except:
log("XXX CACHE Warning: error in set !!!") log("XXX CACHE Warning: error in set !!!")
status = None
return status return status
@classmethod @classmethod

View File

@ -197,4 +197,4 @@ def formsemestre_estim_cost(
coef_tp, coef_tp,
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)

View File

@ -70,7 +70,6 @@ def report_debouche_date(start_year=None, format="html", REQUEST=None):
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
format=format, format=format,
REQUEST=REQUEST,
with_html_headers=True, with_html_headers=True,
) )

View File

@ -1039,7 +1039,7 @@ def formation_table_recap(formation_id, format="html", REQUEST=None):
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
def ue_list_semestre_ids(ue): def ue_list_semestre_ids(ue):

View File

@ -578,7 +578,7 @@ def _view_etuds_page(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
H.append(tab.html()) H.append(tab.html())
@ -678,7 +678,8 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
sem_id = semset["sem_id"] sem_id = semset["sem_id"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id) csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
if format == "raw": if format == "raw":
return scu.sendCSVFile(REQUEST, csv_data, etape_apo + ".txt") scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"]) apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
( (
@ -753,7 +754,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
H += [ H += [
tab.html(), tab.html(),

View File

@ -31,27 +31,21 @@
# Ancien module "scolars" # Ancien module "scolars"
import os import os
import time import time
from flask import url_for, g, request
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email.mime.base import MIMEBase
from operator import itemgetter from operator import itemgetter
from flask import url_for, g, request
from flask_mail import Message
from app import email
from app import log
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import SCO_ENCODING
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import safehtml from app.scodoc import safehtml
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from flask_mail import Message from app.scodoc.TrivialFormulator import TrivialFormulator
from app import mail
MONTH_NAMES_ABBREV = [ MONTH_NAMES_ABBREV = [
"Jan ", "Jan ",
@ -256,7 +250,6 @@ _identiteEditor = ndb.EditableTable(
"photo_filename", "photo_filename",
"code_ine", "code_ine",
"code_nip", "code_nip",
"scodoc7_id",
), ),
filter_dept=True, filter_dept=True,
sortkey="nom", sortkey="nom",
@ -307,7 +300,7 @@ def check_nom_prenom(cnx, nom="", prenom="", etudid=None):
# Don't allow some special cars (eg used in sql regexps) # Don't allow some special cars (eg used in sql regexps)
if scu.FORBIDDEN_CHARS_EXP.search(nom) or scu.FORBIDDEN_CHARS_EXP.search(prenom): if scu.FORBIDDEN_CHARS_EXP.search(nom) or scu.FORBIDDEN_CHARS_EXP.search(prenom):
return False, 0 return False, 0
# Now count homonyms: # Now count homonyms (dans tous les départements):
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
req = """SELECT id req = """SELECT id
FROM identite FROM identite
@ -440,7 +433,7 @@ def notify_etud_change(email_addr, etud, before, after, subject):
"Civilité: " + etud["civilite_str"], "Civilité: " + etud["civilite_str"],
"Nom: " + etud["nom"], "Nom: " + etud["nom"],
"Prénom: " + etud["prenom"], "Prénom: " + etud["prenom"],
"Etudid: " + etud["etudid"], "Etudid: " + str(etud["etudid"]),
"\n", "\n",
"Changements effectués:", "Changements effectués:",
] ]
@ -456,7 +449,7 @@ def notify_etud_change(email_addr, etud, before, after, subject):
log("notify_etud_change: sending notification to %s" % email_addr) log("notify_etud_change: sending notification to %s" % email_addr)
log("notify_etud_change: subject: %s" % subject) log("notify_etud_change: subject: %s" % subject)
log(txt) log(txt)
mail.send_email( email.send_email(
subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt
) )
return txt return txt
@ -896,7 +889,7 @@ def list_scolog(etudid):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"select * from scolog where etudid=%(etudid)s ORDER BY DATE DESC", "SELECT * FROM scolog WHERE etudid=%(etudid)s ORDER BY DATE DESC",
{"etudid": etudid}, {"etudid": etudid},
) )
return cursor.dictfetchall() return cursor.dictfetchall()

View File

@ -305,6 +305,8 @@ class ScoExcelSheet:
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é
""" """
cell = WriteOnlyCell(self.ws, value or "") cell = WriteOnlyCell(self.ws, value or "")
if not (isinstance(value, int) or isinstance(value, float)):
cell.data_type = "s"
# if style is not None and "fill" in style: # if style is not None and "fill" in style:
# toto() # toto()
if style is None: if style is None:

View File

@ -96,3 +96,20 @@ class ScoGenError(ScoException):
class ScoInvalidDateError(ScoValueError): class ScoInvalidDateError(ScoValueError):
pass pass
# Pour les API JSON
class APIInvalidParams(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self)
self.message = message
if status_code is not None:
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv["message"] = self.message
return rv

View File

@ -246,9 +246,7 @@ def scodoc_table_results(
"&types_parcours=".join([str(x) for x in types_parcours]), "&types_parcours=".join([str(x) for x in types_parcours]),
) )
if format != "html": if format != "html":
return tab.make_page( return tab.make_page(format=format, with_html_headers=False)
format=format, with_html_headers=False, REQUEST=REQUEST
)
tab_html = tab.html() tab_html = tab.html()
nb_rows = tab.get_nb_rows() nb_rows = tab.get_nb_rows()
else: else:

View File

@ -225,7 +225,7 @@ def search_etuds_infos(expnom=None, code_nip=None):
else: else:
code_nip = code_nip or expnom code_nip = code_nip or expnom
if code_nip: if code_nip:
etuds = sco_etud.etudident_list(cnx, args={"code_nip": code_nip}) etuds = sco_etud.etudident_list(cnx, args={"code_nip": str(code_nip)})
else: else:
etuds = [] etuds = []
sco_etud.fill_etuds_info(etuds) sco_etud.fill_etuds_info(etuds)
@ -404,6 +404,4 @@ def search_inscr_etud_by_nip(code_nip, REQUEST=None, format="json"):
) )
tab = GenTable(columns_ids=columns_ids, rows=T) tab = GenTable(columns_ids=columns_ids, rows=T)
return tab.make_page( return tab.make_page(format=format, with_html_headers=False, publish=True)
format=format, with_html_headers=False, REQUEST=REQUEST, publish=True
)

View File

@ -27,6 +27,7 @@
"""Operations de base sur les formsemestres """Operations de base sur les formsemestres
""" """
from app.scodoc.sco_exceptions import ScoValueError
import time import time
from operator import itemgetter from operator import itemgetter
@ -60,6 +61,7 @@ _formsemestreEditor = ndb.EditableTable(
"gestion_semestrielle", "gestion_semestrielle",
"etat", "etat",
"bul_hide_xml", "bul_hide_xml",
"block_moyennes",
"bul_bgcolor", "bul_bgcolor",
"modalite", "modalite",
"resp_can_edit", "resp_can_edit",
@ -67,7 +69,6 @@ _formsemestreEditor = ndb.EditableTable(
"ens_can_edit_eval", "ens_can_edit_eval",
"elt_sem_apo", "elt_sem_apo",
"elt_annee_apo", "elt_annee_apo",
"scodoc7_id",
), ),
filter_dept=True, filter_dept=True,
sortkey="date_debut", sortkey="date_debut",
@ -81,6 +82,7 @@ _formsemestreEditor = ndb.EditableTable(
"etat": bool, "etat": bool,
"gestion_compensation": bool, "gestion_compensation": bool,
"bul_hide_xml": bool, "bul_hide_xml": bool,
"block_moyennes": bool,
"gestion_semestrielle": bool, "gestion_semestrielle": bool,
"gestion_compensation": bool, "gestion_compensation": bool,
"gestion_semestrielle": bool, "gestion_semestrielle": bool,
@ -93,6 +95,10 @@ _formsemestreEditor = ndb.EditableTable(
def get_formsemestre(formsemestre_id): def get_formsemestre(formsemestre_id):
"list ONE formsemestre" "list ONE formsemestre"
if not isinstance(formsemestre_id, int):
raise ScoValueError(
"""Semestre invalide, reprenez l'opération au départ ou si le problème persiste signalez l'erreur sur scodoc-devel@listes.univ-paris13.fr"""
)
try: try:
sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0] sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0]
return sem return sem
@ -578,7 +584,7 @@ def view_formsemestre_by_etape(etape_apo=None, format="html", REQUEST=None):
</form>""", </form>""",
) )
tab.base_url = "%s?etape_apo=%s" % (REQUEST.URL0, etape_apo or "") tab.base_url = "%s?etape_apo=%s" % (REQUEST.URL0, etape_apo or "")
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
def sem_has_etape(sem, code_etape): def sem_has_etape(sem, code_etape):

View File

@ -501,6 +501,14 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
"labels": [""], "labels": [""],
}, },
), ),
(
"block_moyennes",
{
"input_type": "boolcheckbox",
"title": "Bloquer moyennes",
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
},
),
( (
"sep", "sep",
{ {
@ -693,7 +701,6 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
tf[2]["bul_hide_xml"] = False tf[2]["bul_hide_xml"] = False
else: else:
tf[2]["bul_hide_xml"] = True tf[2]["bul_hide_xml"] = True
# remap les identifiants de responsables: # remap les identifiants de responsables:
tf[2]["responsable_id"] = User.get_user_id_from_nomplogin( tf[2]["responsable_id"] = User.get_user_id_from_nomplogin(
tf[2]["responsable_id"] tf[2]["responsable_id"]

View File

@ -64,7 +64,7 @@ from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
def _build_menu_stats(formsemestre_id): def _build_menu_stats(formsemestre_id):
"Définition du menu 'Statistiques' " "Définition du menu 'Statistiques'"
return [ return [
{ {
"title": "Statistiques...", "title": "Statistiques...",
@ -495,6 +495,7 @@ def formsemestre_page_title():
if not formsemestre_id: if not formsemestre_id:
return "" return ""
try: try:
formsemestre_id = int(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy() sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy()
except: except:
log("can't find formsemestre_id %s" % formsemestre_id) log("can't find formsemestre_id %s" % formsemestre_id)
@ -727,7 +728,7 @@ def formsemestre_description(
tab.html_before_table += "checked" tab.html_before_table += "checked"
tab.html_before_table += ">indiquer les évaluations</input></form>" tab.html_before_table += ">indiquer les évaluations</input></form>"
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
# genere liste html pour accès aux groupes de ce semestre # genere liste html pour accès aux groupes de ce semestre
@ -913,10 +914,10 @@ def formsemestre_status_head(formsemestre_id=None, REQUEST=None, page_title=None
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST, page_title, sem, with_page_header=False, with_h2=False REQUEST, page_title, sem, with_page_header=False, with_h2=False
), ),
"""<table> f"""<table>
<tr><td class="fichetitre2">Formation: </td><td> <tr><td class="fichetitre2">Formation: </td><td>
<a href="Notes/ue_list?formation_id=%(formation_id)s" class="discretelink" title="Formation %(acronyme)s, v%(version)s">%(titre)s</a>""" <a href="{url_for('notes.ue_list', scodoc_dept=g.scodoc_dept, formation_id=F['formation_id'])}"
% F, class="discretelink" title="Formation {F['acronyme']}, v{F['version']}">{F['titre']}</a>""",
] ]
if sem["semestre_id"] >= 0: if sem["semestre_id"] >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"])) H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"]))
@ -947,10 +948,13 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
</td></tr>""" </td></tr>"""
) )
H.append("</table>") H.append("</table>")
sem_warning = ""
if sem["bul_hide_xml"]: if sem["bul_hide_xml"]:
H.append( sem_warning += "Bulletins non publiés sur le portail. "
'<p class="fontorange"><em>Bulletins non publiés sur le portail</em></p>' if sem["block_moyennes"]:
) sem_warning += "Calcul des moyennes bloqué !"
if sem_warning:
H.append('<p class="fontorange"><em>' + sem_warning + "</em></p>")
if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(sem): if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(sem):
H.append( H.append(
'<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>' '<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'

View File

@ -1499,7 +1499,7 @@ def _sortgroups(groups):
# Tri: place 'all' en tête, puis groupe par partition / nom de groupe # Tri: place 'all' en tête, puis groupe par partition / nom de groupe
R = [g for g in groups if g["partition_name"] is None] R = [g for g in groups if g["partition_name"] is None]
o = [g for g in groups if g["partition_name"] != None] o = [g for g in groups if g["partition_name"] != None]
o.sort(key=lambda x: (x["numero"], x["group_name"])) o.sort(key=lambda x: (x["numero"] or 0, x["group_name"]))
return R + o return R + o

View File

@ -477,6 +477,9 @@ def groups_table(
[(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions]
) )
) )
partitions_name = {
p["partition_id"]: p["partition_name"] for p in groups_infos.partitions
}
if format != "html": # ne mentionne l'état que en Excel (style en html) if format != "html": # ne mentionne l'état que en Excel (style en html)
columns_ids.append("etat") columns_ids.append("etat")
@ -500,11 +503,7 @@ def groups_table(
if with_annotations: if with_annotations:
sco_etud.add_annotations_to_etud_list(groups_infos.members) sco_etud.add_annotations_to_etud_list(groups_infos.members)
columns_ids += ["annotations_str"] columns_ids += ["annotations_str"]
moodle_sem_name = groups_infos.formsemestre["session_id"]
if groups_infos.formsemestre["semestre_id"] >= 0:
moodle_sem_name = "S%d" % groups_infos.formsemestre["semestre_id"]
else:
moodle_sem_name = "A" # pas de semestre spécifié, que faire ?
moodle_groupenames = set() moodle_groupenames = set()
# ajoute liens # ajoute liens
for etud in groups_infos.members: for etud in groups_infos.members:
@ -529,23 +528,27 @@ def groups_table(
# et groupes: # et groupes:
for partition_id in etud["partitions"]: for partition_id in etud["partitions"]:
etud[partition_id] = etud["partitions"][partition_id]["group_name"] etud[partition_id] = etud["partitions"][partition_id]["group_name"]
# Ajoute colonne pour moodle: semestre_groupe, de la forme S1-NomgroupeXXX # Ajoute colonne pour moodle: semestre_groupe, de la forme RT-DUT-FI-S3-2021-PARTITION-GROUPE
moodle_groupename = [] moodle_groupename = []
if groups_infos.selected_partitions: if groups_infos.selected_partitions:
# il y a des groupes selectionnes, utilise leurs partitions # il y a des groupes selectionnes, utilise leurs partitions
for partition_id in groups_infos.selected_partitions: for partition_id in groups_infos.selected_partitions:
if partition_id in etud["partitions"]: if partition_id in etud["partitions"]:
moodle_groupename.append( moodle_groupename.append(
etud["partitions"][partition_id]["group_name"] partitions_name[partition_id]
+ "-"
+ etud["partitions"][partition_id]["group_name"]
) )
else: else:
# pas de groupes sélectionnés: prend le premier s'il y en a un # pas de groupes sélectionnés: prend le premier s'il y en a un
moodle_groupename = ["tous"]
if etud["partitions"]: if etud["partitions"]:
for p in etud["partitions"].items(): # partitions is an OrderedDict for p in etud["partitions"].items(): # partitions is an OrderedDict
moodle_groupename = [
partitions_name[p[0]] + "-" + p[1]["group_name"]
]
break break
moodle_groupename = [p[1]["group_name"]]
else:
moodle_groupename = ["tous"]
moodle_groupenames |= set(moodle_groupename) moodle_groupenames |= set(moodle_groupename)
etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename)
@ -706,7 +709,7 @@ def groups_table(
): ):
if format == "moodlecsv": if format == "moodlecsv":
format = "csv" format = "csv"
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
elif format == "xlsappel": elif format == "xlsappel":
xls = sco_excel.excel_feuille_listeappel( xls = sco_excel.excel_feuille_listeappel(
@ -935,7 +938,7 @@ def form_choix_saisie_semaine(groups_infos, REQUEST=None):
return "\n".join(FA) return "\n".join(FA)
def export_groups_as_moodle_csv(formsemestre_id=None, REQUEST=None): def export_groups_as_moodle_csv(formsemestre_id=None):
"""Export all students/groups, in a CSV format suitable for Moodle """Export all students/groups, in a CSV format suitable for Moodle
Each (student,group) will be listed on a separate line Each (student,group) will be listed on a separate line
jo@univ.fr,S3-A jo@univ.fr,S3-A
@ -977,4 +980,4 @@ def export_groups_as_moodle_csv(formsemestre_id=None, REQUEST=None):
text_with_titles=prefs["moodle_csv_with_headerline"], text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs, preferences=prefs,
) )
return tab.make_page(format="csv", REQUEST=REQUEST) return tab.make_page(format="csv")

View File

@ -139,18 +139,22 @@ def list_etuds_from_sem(src, dst):
def list_inscrits_date(sem): def list_inscrits_date(sem):
"""Liste les etudiants inscrits dans n'importe quel semestre """Liste les etudiants inscrits dans n'importe quel semestre
du même département
SAUF sem à la date de début de sem. SAUF sem à la date de début de sem.
""" """
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"]) sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
cursor.execute( cursor.execute(
"""SELECT I.etudid """SELECT ins.etudid
FROM notes_formsemestre_inscription I, notes_formsemestre S FROM
WHERE I.formsemestre_id = S.id notes_formsemestre_inscription ins,
notes_formsemestre S
WHERE ins.formsemestre_id = S.id
AND S.id != %(formsemestre_id)s AND S.id != %(formsemestre_id)s
AND S.date_debut <= %(date_debut_iso)s AND S.date_debut <= %(date_debut_iso)s
AND S.date_fin >= %(date_debut_iso)s AND S.date_fin >= %(date_debut_iso)s
AND S.dept_id = %(dept_id)s
""", """,
sem, sem,
) )

View File

@ -27,8 +27,8 @@
"""Liste des notes d'une évaluation """Liste des notes d'une évaluation
""" """
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
from operator import itemgetter from operator import itemgetter
import urllib
import flask import flask
from flask import url_for, g from flask import url_for, g
@ -50,6 +50,7 @@ from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_users
import sco_version import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.htmlutils import histogram_notes from app.scodoc.htmlutils import histogram_notes
@ -481,7 +482,7 @@ def _make_table_notes(
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete # html_generate_cells=False # la derniere ligne (moyennes) est incomplete
) )
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
@ -569,7 +570,7 @@ def _add_eval_columns(
comment = "" comment = ""
explanation = "%s (%s) %s" % ( explanation = "%s (%s) %s" % (
NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"), NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
NotesDB[etudid]["uid"], sco_users.user_info(NotesDB[etudid]["uid"])["nomcomplet"],
comment, comment,
) )
else: else:
@ -816,8 +817,8 @@ def evaluation_check_absences_html(
'<a class="stdlink" href="Absences/doSignaleAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s&moduleimpl_id=%s">signaler cette absence</a>' '<a class="stdlink" href="Absences/doSignaleAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s&moduleimpl_id=%s">signaler cette absence</a>'
% ( % (
etud["etudid"], etud["etudid"],
six.moves.urllib.parse.quote(E["jour"]), urllib.parse.quote(E["jour"]),
six.moves.urllib.parse.quote(E["jour"]), urllib.parse.quote(E["jour"]),
demijournee, demijournee,
E["moduleimpl_id"], E["moduleimpl_id"],
) )

View File

@ -85,7 +85,7 @@ def scodoc_table_etuds_lycees(format="html", REQUEST=None):
no_links=True, no_links=True,
) )
tab.base_url = REQUEST.URL0 tab.base_url = REQUEST.URL0
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
H = [ H = [
@ -192,7 +192,7 @@ def formsemestre_etuds_lycees(
tab.base_url += "&only_primo=1" tab.base_url += "&only_primo=1"
if no_grouping: if no_grouping:
tab.base_url += "&no_grouping=1" tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
F = [ F = [

View File

@ -529,10 +529,10 @@ def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id, REQUEST=None):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""DELETE FROM notes_moduleimpl_inscription """DELETE FROM notes_moduleimpl_inscription
WHERE moduleimpl_inscription_id IN ( WHERE moduleimpl_inscription_id IN (
SELECT i.moduleimpl_inscription_id FROM SELECT i.moduleimpl_inscription_id FROM
notes_moduleimpl mi, notes_modules mod, notes_moduleimpl mi, notes_modules mod,
notes_formsemestre sem, notes_moduleimpl_inscription i notes_formsemestre sem, notes_moduleimpl_inscription i
WHERE sem.formsemestre_id = %(formsemestre_id)s WHERE sem.formsemestre_id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.formsemestre_id AND mi.formsemestre_id = sem.formsemestre_id

View File

@ -28,7 +28,7 @@
"""Tableau de bord module """Tableau de bord module
""" """
import time import time
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import urllib
from flask import g, url_for from flask import g, url_for
from flask_login import current_user from flask_login import current_user
@ -137,7 +137,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"title": "Absences ce jour", "title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate", "endpoint": "absences.EtatAbsencesDate",
"args": { "args": {
"date": six.moves.urllib.parse.quote(E["jour"], safe=""), "date": urllib.parse.quote(E["jour"], safe=""),
"group_ids": group_id, "group_ids": group_id,
}, },
"enabled": E["jour"], "enabled": E["jour"],

View File

@ -45,13 +45,20 @@ class Permission(object):
NBITS = 1 # maximum bits used (for formatting) NBITS = 1 # maximum bits used (for formatting)
ALL_PERMISSIONS = [-1] ALL_PERMISSIONS = [-1]
description = {} # { symbol : blah blah } description = {} # { symbol : blah blah }
permission_by_name = {} # { symbol : int }
@staticmethod @staticmethod
def init_permissions(): def init_permissions():
for (perm, symbol, description) in _SCO_PERMISSIONS: for (perm, symbol, description) in _SCO_PERMISSIONS:
setattr(Permission, symbol, perm) setattr(Permission, symbol, perm)
Permission.description[symbol] = description Permission.description[symbol] = description
Permission.permission_by_name[symbol] = perm
Permission.NBITS = len(_SCO_PERMISSIONS) Permission.NBITS = len(_SCO_PERMISSIONS)
@staticmethod
def get_by_name(permission_name: str) -> int:
"""Return permission mode (integer bit field), or None if it doesn't exist."""
return Permission.permission_by_name.get(permission_name)
Permission.init_permissions() Permission.init_permissions()

View File

@ -217,6 +217,5 @@ def formsemestre_poursuite_report(formsemestre_id, format="html", REQUEST=None):
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
format=format, format=format,
REQUEST=REQUEST,
with_html_headers=True, with_html_headers=True,
) )

View File

@ -112,6 +112,7 @@ get_base_preferences(formsemestre_id)
""" """
import flask import flask
from flask import g, url_for from flask import g, url_for
from flask_login import current_user
from app.models import Departement from app.models import Departement
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -180,7 +181,7 @@ def _convert_pref_type(p, pref_spec):
def _get_pref_default_value_from_config(name, pref_spec): def _get_pref_default_value_from_config(name, pref_spec):
"""get default value store in application level config. """get default value store in application level config.
If not found, use defalut value hardcoded in pref_spec. If not found, use default value hardcoded in pref_spec.
""" """
# XXX va changer avec la nouvelle base # XXX va changer avec la nouvelle base
# search in scu.CONFIG # search in scu.CONFIG
@ -1408,7 +1409,7 @@ class BasePreferences(object):
{ {
"initvalue": 1, "initvalue": 1,
"title": "Indique si les bulletins sont publiés", "title": "Indique si les bulletins sont publiés",
"explanation": "décocher si vous n'avez pas de portal étudiant publiant les bulletins", "explanation": "décocher si vous n'avez pas de portail étudiant publiant les bulletins",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "bul", "category": "bul",
@ -1891,7 +1892,7 @@ class BasePreferences(object):
def get(self, formsemestre_id, name): def get(self, formsemestre_id, name):
"""Returns preference value. """Returns preference value.
If global_lookup, when no value defined for this semestre, returns global value. when no value defined for this semestre, returns global value.
""" """
params = { params = {
"dept_id": self.dept_id, "dept_id": self.dept_id,
@ -1901,7 +1902,7 @@ class BasePreferences(object):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
plist = self._editor.list(cnx, params) plist = self._editor.list(cnx, params)
if not plist: if not plist:
del params["formsemestre_id"] params["formsemestre_id"] = None
plist = self._editor.list(cnx, params) plist = self._editor.list(cnx, params)
if not plist: if not plist:
return self.default[name] return self.default[name]
@ -2022,7 +2023,9 @@ class BasePreferences(object):
html_sco_header.sco_header(page_title="Préférences"), html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(), "<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept) f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
}">modification des logos du département (pour documents pdf)</a></p>""", }">modification des logos du département (pour documents pdf)</a></p>"""
if current_user.is_administrator()
else "",
"""<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p> """<p class="help">Ces paramètres s'appliquent par défaut à tous les semestres, sauf si ceux-ci définissent des valeurs spécifiques.</p>
<p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p> <p class="msg">Attention: cliquez sur "Enregistrer les modifications" en bas de page pour appliquer vos changements !</p>
""", """,
@ -2253,7 +2256,7 @@ function set_global_pref(el, pref_name) {
# #
def doc_preferences(): def doc_preferences():
""" Liste les preferences en MarkDown, pour la documentation""" """Liste les preferences en MarkDown, pour la documentation"""
L = [] L = []
for cat, cat_descr in PREF_CATEGORIES: for cat, cat_descr in PREF_CATEGORIES:
L.append([""]) L.append([""])

View File

@ -535,7 +535,6 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True, REQUEST=No
return tab.make_page( return tab.make_page(
format=format, format=format,
with_html_headers=False, with_html_headers=False,
REQUEST=REQUEST,
publish=publish, publish=publish,
) )
tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id) tab.base_url = "%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id)

View File

@ -27,8 +27,9 @@
"""Tableau recapitulatif des notes d'un semestre """Tableau recapitulatif des notes d'un semestre
""" """
import time
import datetime import datetime
import json
import time
from xml.etree import ElementTree from xml.etree import ElementTree
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -227,11 +228,14 @@ def do_formsemestre_recapcomplet(
if format == "xml" or format == "html": if format == "xml" or format == "html":
return data return data
elif format == "csv": elif format == "csv":
return scu.sendCSVFile(REQUEST, data, filename) return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
elif format[:3] == "xls": elif format[:3] == "xls" or format[:3] == "xlsx":
return sco_excel.send_excel_file(REQUEST, data, filename) return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE)
elif format == "json": elif format == "json":
return scu.sendJSON(REQUEST, data) js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
return scu.send_file(
js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
)
else: else:
raise ValueError("unknown format %s" % format) raise ValueError("unknown format %s" % format)

View File

@ -355,7 +355,6 @@ def formsemestre_report_counts(
t = tab.make_page( t = tab.make_page(
title="""<h2 class="formsemestre">Comptes croisés</h2>""", title="""<h2 class="formsemestre">Comptes croisés</h2>""",
format=format, format=format,
REQUEST=REQUEST,
with_html_headers=False, with_html_headers=False,
) )
if format != "html": if format != "html":
@ -722,7 +721,7 @@ def formsemestre_suivi_cohorte(
) )
if only_primo: if only_primo:
tab.base_url += "&only_primo=on" tab.base_url += "&only_primo=on"
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
@ -1210,7 +1209,7 @@ def formsemestre_suivi_parcours(
tab.base_url += "&only_primo=1" tab.base_url += "&only_primo=1"
if no_grouping: if no_grouping:
tab.base_url += "&no_grouping=1" tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) t = tab.make_page(format=format, with_html_headers=False)
if format != "html": if format != "html":
return t return t
F = [ F = [

View File

@ -9,48 +9,50 @@ from app.scodoc.sco_permissions import Permission as p
SCO_ROLES_DEFAULTS = { SCO_ROLES_DEFAULTS = {
"Observateur": (p.ScoObservateur,), "Observateur": (p.ScoObservateur,),
"Ens": ( "Ens": (
p.ScoObservateur,
p.ScoView,
p.ScoEnsView,
p.ScoUsersView,
p.ScoEtudAddAnnotations,
p.ScoAbsChange,
p.ScoAbsAddBillet, p.ScoAbsAddBillet,
p.ScoAbsChange,
p.ScoEnsView,
p.ScoEntrepriseView, p.ScoEntrepriseView,
p.ScoEtudAddAnnotations,
p.ScoObservateur,
p.ScoUsersView,
p.ScoView,
), ),
"Secr": ( "Secr": (
p.ScoObservateur,
p.ScoView,
p.ScoUsersView,
p.ScoEtudAddAnnotations,
p.ScoAbsChange,
p.ScoAbsAddBillet, p.ScoAbsAddBillet,
p.ScoEntrepriseView, p.ScoAbsChange,
p.ScoEditApo,
p.ScoEntrepriseChange, p.ScoEntrepriseChange,
p.ScoEntrepriseView,
p.ScoEtudAddAnnotations,
p.ScoEtudChangeAdr, p.ScoEtudChangeAdr,
p.ScoObservateur,
p.ScoUsersView,
p.ScoView,
), ),
# Admin est le chef du département, pas le "super admin" # Admin est le chef du département, pas le "super admin"
# on doit donc lister toutes ses permissions: # on doit donc lister toutes ses permissions:
"Admin": ( "Admin": (
p.ScoObservateur,
p.ScoView,
p.ScoEnsView,
p.ScoUsersView,
p.ScoEtudAddAnnotations,
p.ScoAbsChange,
p.ScoAbsAddBillet, p.ScoAbsAddBillet,
p.ScoEntrepriseView, p.ScoAbsChange,
p.ScoEntrepriseChange,
p.ScoEtudChangeAdr,
p.ScoChangeFormation, p.ScoChangeFormation,
p.ScoEditFormationTags, p.ScoChangePreferences,
p.ScoEditAllNotes,
p.ScoEditAllEvals, p.ScoEditAllEvals,
p.ScoImplement, p.ScoEditAllNotes,
p.ScoEditApo,
p.ScoEditFormationTags,
p.ScoEnsView,
p.ScoEntrepriseChange,
p.ScoEntrepriseView,
p.ScoEtudAddAnnotations,
p.ScoEtudChangeAdr,
p.ScoEtudChangeGroups, p.ScoEtudChangeGroups,
p.ScoEtudInscrit, p.ScoEtudInscrit,
p.ScoImplement,
p.ScoObservateur,
p.ScoUsersAdmin, p.ScoUsersAdmin,
p.ScoChangePreferences, p.ScoUsersView,
p.ScoView,
), ),
# RespPE est le responsable poursuites d'études # RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations: # il peut ajouter des tags sur les formations:

View File

@ -494,9 +494,10 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
} }
ndb.quote_dict(aa) ndb.quote_dict(aa)
cursor.execute( cursor.execute(
"""INSERT INTO notes_notes """INSERT INTO notes_notes
(etudid,evaluation_id,value,comment,date,uid) (etudid, evaluation_id, value, comment, date, uid)
VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)""", VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)
""",
aa, aa,
) )
changed = True changed = True
@ -515,10 +516,10 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
# recopie l'ancienne note dans notes_notes_log, puis update # recopie l'ancienne note dans notes_notes_log, puis update
if do_it: if do_it:
cursor.execute( cursor.execute(
"""INSERT INTO notes_notes_log """INSERT INTO notes_notes_log
(etudid,evaluation_id,value,comment,date,uid) (etudid,evaluation_id,value,comment,date,uid)
SELECT etudid, evaluation_id, value, comment, date, uid SELECT etudid, evaluation_id, value, comment, date, uid
FROM notes_notes FROM notes_notes
WHERE etudid=%(etudid)s WHERE etudid=%(etudid)s
and evaluation_id=%(evaluation_id)s and evaluation_id=%(evaluation_id)s
""", """,
@ -536,8 +537,8 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
if value != scu.NOTES_SUPPRESS: if value != scu.NOTES_SUPPRESS:
if do_it: if do_it:
cursor.execute( cursor.execute(
"""UPDATE notes_notes """UPDATE notes_notes
SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
WHERE etudid = %(etudid)s WHERE etudid = %(etudid)s
and evaluation_id = %(evaluation_id)s and evaluation_id = %(evaluation_id)s
""", """,
@ -550,7 +551,7 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
% (evaluation_id, etudid, oldval) % (evaluation_id, etudid, oldval)
) )
cursor.execute( cursor.execute(
"""DELETE FROM notes_notes """DELETE FROM notes_notes
WHERE etudid = %(etudid)s WHERE etudid = %(etudid)s
AND evaluation_id = %(evaluation_id)s AND evaluation_id = %(evaluation_id)s
""", """,
@ -589,18 +590,17 @@ def _notes_add(user, evaluation_id: int, notes: list, comment=None, do_it=True):
def saisie_notes_tableur(evaluation_id, group_ids=[], REQUEST=None): def saisie_notes_tableur(evaluation_id, group_ids=[], REQUEST=None):
"""Saisie des notes via un fichier Excel""" """Saisie des notes via un fichier Excel"""
authuser = REQUEST.AUTHENTICATED_USER
authusername = str(authuser)
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals: if not evals:
raise ScoValueError("invalid evaluation_id") raise ScoValueError("invalid evaluation_id")
E = evals[0] E = evals[0]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]): if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
return ( return (
html_sco_header.sco_header() html_sco_header.sco_header()
+ "<h2>Modification des notes impossible pour %s</h2>" % authusername + "<h2>Modification des notes impossible pour %s</h2>"
% current_user.user_name
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p> avez l'autorisation d'effectuer cette opération)</p>
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p> <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
@ -858,9 +858,7 @@ def has_existing_decision(M, E, etudid):
def saisie_notes(evaluation_id, group_ids=[], REQUEST=None): def saisie_notes(evaluation_id, group_ids=[], REQUEST=None):
"""Formulaire saisie notes d'une évaluation pour un groupe""" """Formulaire saisie notes d'une évaluation pour un groupe"""
authuser = REQUEST.AUTHENTICATED_USER group_ids = [int(group_id) for group_id in group_ids]
authusername = str(authuser)
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals: if not evals:
raise ScoValueError("invalid evaluation_id") raise ScoValueError("invalid evaluation_id")
@ -871,10 +869,11 @@ def saisie_notes(evaluation_id, group_ids=[], REQUEST=None):
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
# Check access # Check access
# (admin, respformation, and responsable_id) # (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]): if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
return ( return (
html_sco_header.sco_header() html_sco_header.sco_header()
+ "<h2>Modification des notes impossible pour %s</h2>" % authusername + "<h2>Modification des notes impossible pour %s</h2>"
% current_user.user_name
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p> avez l'autorisation d'effectuer cette opération)</p>
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p> <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
@ -1154,9 +1153,9 @@ def _form_saisie_notes(E, M, group_ids, destination="", REQUEST=None):
"attributes": [ "attributes": [
'class="note%s"' % classdem, 'class="note%s"' % classdem,
disabled_attr, disabled_attr,
"data-last-saved-value=%s" % e["val"], 'data-last-saved-value="%s"' % e["val"],
"data-orig-value=%s" % e["val"], 'data-orig-value="%s"' % e["val"],
"data-etudid=%s" % etudid, 'data-etudid="%s"' % etudid,
], ],
"template": """<tr%(item_dom_attr)s class="etud_elem """ "template": """<tr%(item_dom_attr)s class="etud_elem """
+ " ".join(etud_classes) + " ".join(etud_classes)
@ -1290,7 +1289,7 @@ def get_note_history_menu(evaluation_id, etudid):
nv = "" # ne repete pas la valeur de la note courante nv = "" # ne repete pas la valeur de la note courante
else: else:
# ancienne valeur # ancienne valeur
nv = '<span class="histvalue">: %s</span>' % dispnote nv = ": %s" % dispnote
first = False first = False
if i["comment"]: if i["comment"]:
comment = ' <span class="histcomment">%s</span>' % i["comment"] comment = ' <span class="histcomment">%s</span>' % i["comment"]

View File

@ -418,7 +418,7 @@ def do_semset_remove_sem(semset_id, formsemestre_id):
# ---------------------------------------- # ----------------------------------------
def semset_page(format="html", REQUEST=None): def semset_page(format="html"):
"""Page avec liste semsets: """Page avec liste semsets:
Table avec : date_debut date_fin titre liste des semestres Table avec : date_debut date_fin titre liste des semestres
""" """
@ -468,7 +468,7 @@ def semset_page(format="html", REQUEST=None):
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
page_title = "Ensembles de semestres" page_title = "Ensembles de semestres"
H = [ H = [

View File

@ -400,7 +400,10 @@ def list_synch(sem, anneeapogee=None):
def key2etud(key, etud_apo=False): def key2etud(key, etud_apo=False):
if not etud_apo: if not etud_apo:
etudid = key2etudid[key] etudid = key2etudid[key]
etud = sco_etud.identite_list(cnx, {"etudid": etudid})[0] etuds = sco_etud.identite_list(cnx, {"etudid": etudid})
if not etuds: # ? cela ne devrait pas arriver XXX
log(f"XXX key2etud etudid={{etudid}}, type {{type(etudid)}}")
etud = etuds[0]
etud["inscrit"] = is_inscrit # checkbox state etud["inscrit"] = is_inscrit # checkbox state
etud[ etud[
"datefinalisationinscription" "datefinalisationinscription"
@ -508,7 +511,14 @@ def list_all(etudsapo_set):
# d'interrogation par etudiant. # d'interrogation par etudiant.
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute("SELECT " + EKEY_SCO + ", id AS etudid FROM identite") cursor.execute(
"SELECT "
+ EKEY_SCO
+ """, id AS etudid
FROM identite WHERE dept_id=%(dept_id)s
""",
{"dept_id": g.scodoc_dept_id},
)
key2etudid = dict([(x[0], x[1]) for x in cursor.fetchall()]) key2etudid = dict([(x[0], x[1]) for x in cursor.fetchall()])
all_set = set(key2etudid.keys()) all_set = set(key2etudid.keys())

View File

@ -167,7 +167,7 @@ def evaluation_list_operations(evaluation_id, REQUEST=None):
% (E["description"], E["jour"]), % (E["description"], E["jour"]),
preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), preferences=sco_preferences.SemPreferences(M["formsemestre_id"]),
) )
return tab.make_page(REQUEST=REQUEST) return tab.make_page()
def formsemestre_list_saisies_notes(formsemestre_id, format="html", REQUEST=None): def formsemestre_list_saisies_notes(formsemestre_id, format="html", REQUEST=None):
@ -222,7 +222,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html", REQUEST=None
+ scu.timedate_human_repr() + scu.timedate_human_repr()
+ "", + "",
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
def get_note_history(evaluation_id, etudid, REQUEST=None, fmt=""): def get_note_history(evaluation_id, etudid, REQUEST=None, fmt=""):

View File

@ -217,7 +217,7 @@ def list_users(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
return tab.make_page(format=format, with_html_headers=False, REQUEST=REQUEST) return tab.make_page(format=format, with_html_headers=False)
def get_user_list(dept=None, with_inactives=False): def get_user_list(dept=None, with_inactives=False):
@ -258,6 +258,7 @@ def user_info(user_name_or_id=None, user=None):
info = u.to_dict() info = u.to_dict()
else: else:
info = None info = None
user_name = "inconnu"
else: else:
info = user.to_dict() info = user.to_dict()
user_name = user.user_name user_name = user.user_name

View File

@ -2612,7 +2612,8 @@ div.maindiv {
margin: 1em; margin: 1em;
} }
ul.main { ul.main {
list-style-type: square; list-style-type: square;
margin-top: 1em;
} }
ul.main li { ul.main li {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

25
app/templates/about.html Normal file
View File

@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h2>Système de gestion scolarité</h2>
<p>&copy; Emmanuel Viennet 2021</p>
<p>Version {{ version }}</p>
<p>ScoDoc est un logiciel libre écrit en
<a href="http://www.python.org" target="_blank" rel="noopener noreferrer">Python</a>.
Information et documentation sur <a href="https://scodoc.org" target="_blank">scodoc.org</a>.
</p>
<h2>Dernières évolutions</h2>
{{ news|safe }}
<div class="about-logo">
{{ logo|safe }}
</div>
{% endblock %}

View File

@ -2,14 +2,19 @@
{% import 'bootstrap/wtf.html' as wtf %} {% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %} {% block app_content %}
<h1>Sign In</h1> <h1>Connexion</h1>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
{{ wtf.quick_form(form) }} {{ wtf.quick_form(form) }}
</div> </div>
</div> </div>
<br> <br>
Forgot Your Password? En cas d'oubli de votre mot de passe
<a href="{{ url_for('auth.reset_password_request') }}">Click to Reset It</a> <a href="{{ url_for('auth.reset_password_request') }}">cliquez ici pour le réinitialiser</a>.
</p> </p>
<p class="help">L'accès à ScoDoc est strictement réservé aux personnels de
l'établissement. Les étudiants n'y ont pas accès. Pour toute information,
contactez la personne responsable de votre établissement.</p>
{% endblock %} {% endblock %}

View File

@ -11,7 +11,7 @@
ou écrire la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en ou écrire la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en
indiquant la version du logiciel indiquant la version du logiciel
<br /> <br />
(plus d'informations sur les listes de diffusion<a href="https://scodoc.org/ListesDeDiffusion/">voir (plus d'informations sur les listes de diffusion <a href="https://scodoc.org/ListesDeDiffusion/">voir
cette page</a>). cette page</a>).
</p> </p>

View File

@ -24,9 +24,9 @@
<p> Si le problème persiste après intervention de votre équipe locale, <p> Si le problème persiste après intervention de votre équipe locale,
contacter la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en contacter la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en
indiquant la version du logiciel (ScoDoc {SCOVERSION}) indiquant la version du logiciel (ScoDoc {{SCOVERSION}})
<br />(pour plus d'informations sur les listes de diffusion <a <br />(pour plus d'informations sur les listes de diffusion
href="https://scodoc.org/ListesDeDiffusion/">voir cette page</a>). <a href="https://scodoc.org/ListesDeDiffusion/">voir cette page</a>).
</p> </p>
</body> </body>

View File

@ -2,7 +2,7 @@
{% import 'bootstrap/wtf.html' as wtf %} {% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %} {% block app_content %}
<h2>ScoDoc: gestion scolarité (version béta)</h2> <h2>ScoDoc 9 - suivi scolarité</h2>
{% if not current_user.is_anonymous %} {% if not current_user.is_anonymous %}
<p>Bonjour <font color="red"><b>{{current_user.get_nomcomplet()}}</b> <p>Bonjour <font color="red"><b>{{current_user.get_nomcomplet()}}</b>
@ -24,10 +24,6 @@
{% endfor %} {% endfor %}
</ul> </ul>
<p>
<font color="red">Ceci est une version de test,
ne pas utiliser en production !</font>
</p>
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<form action="{{url_for('scodoc.table_etud_in_accessible_depts')}}" method="POST"> <form action="{{url_for('scodoc.table_etud_in_accessible_depts')}}" method="POST">
@ -43,4 +39,9 @@
<p><a href="/ScoDoc/static/mobile">Charger la version mobile (expérimentale)</a></p> <p><a href="/ScoDoc/static/mobile">Charger la version mobile (expérimentale)</a></p>
</div> --> </div> -->
<div style="margin-top: 1cm;">
Service réservé aux personnels et enseignants, basé sur <a href="{{url_for('scodoc.about')}}">le logiciel libre
ScoDoc.</a>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,12 @@
<h2 class="insidebar">Dépt. {{ prefs["DeptName"] }}</h2>
<a href="{{ url_for('scodoc.index') }}" class="sidebar">Accueil</a> <br/>
{% if prefs["DeptIntranetURL"] %}
<a href="{{ prefs["DeptIntranetURL"] }}" class="sidebar">
{{ prefs["DeptIntranetTitle"] }}</a>
{% endif %}
<br/>
{#
# Entreprises pas encore supporté en ScoDoc8
# <br/><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br/>
#}

View File

@ -47,20 +47,17 @@ L'API de plus bas niveau est en gros:
""" """
import calendar import calendar
import cgi
import datetime import datetime
import dateutil import dateutil
import dateutil.parser import dateutil.parser
import re import re
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
import string
import time import time
import urllib
from xml.etree import ElementTree from xml.etree import ElementTree
import flask import flask
from flask import g from flask import g
from flask import url_for from flask import url_for
from flask import current_app
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
@ -79,7 +76,7 @@ from app.scodoc import notesdb as ndb
from app import log from app import log
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -277,8 +274,9 @@ def doSignaleAbsenceGrSemestre(
Efface les absences aux dates indiquées par dates, Efface les absences aux dates indiquées par dates,
ou bien ajoute celles de abslist. ou bien ajoute celles de abslist.
""" """
moduleimpl_id = moduleimpl_id or None
if etudids: if etudids:
etudids = etudids.split(",") etudids = [int(x) for x in str(etudids).split(",")]
else: else:
etudids = [] etudids = []
if dates: if dates:
@ -306,14 +304,14 @@ def doSignaleAbsenceGrSemestre(
@permission_required(Permission.ScoAbsChange) @permission_required(Permission.ScoAbsChange)
@scodoc7func @scodoc7func
def SignaleAbsenceGrHebdo( def SignaleAbsenceGrHebdo(
datelundi, group_ids=[], destination="", moduleimpl_id=None, REQUEST=None datelundi, group_ids=[], destination="", moduleimpl_id=None, formsemestre_id=None
): ):
"Saisie hebdomadaire des absences" "Saisie hebdomadaire des absences"
if not moduleimpl_id: if not moduleimpl_id:
moduleimpl_id = None moduleimpl_id = None
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, moduleimpl_id=moduleimpl_id group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
) )
if not groups_infos.members: if not groups_infos.members:
return ( return (
@ -325,7 +323,7 @@ def SignaleAbsenceGrHebdo(
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % ( base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
datelundi, datelundi,
groups_infos.groups_query_args, groups_infos.groups_query_args,
six.moves.urllib.parse.quote(destination), urllib.parse.quote(destination),
) )
formsemestre_id = groups_infos.formsemestre_id formsemestre_id = groups_infos.formsemestre_id
@ -509,7 +507,7 @@ def SignaleAbsenceGrSemestre(
datedebut, datedebut,
datefin, datefin,
groups_infos.groups_query_args, groups_infos.groups_query_args,
six.moves.urllib.parse.quote(destination), urllib.parse.quote(destination),
) )
) )
base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id
@ -809,7 +807,7 @@ def _gen_form_saisie_groupe(
H.append('<input type="hidden" name="dates" value="%s"/>' % ",".join(dates)) H.append('<input type="hidden" name="dates" value="%s"/>' % ",".join(dates))
H.append( H.append(
'<input type="hidden" name="destination" value="%s"/>' '<input type="hidden" name="destination" value="%s"/>'
% six.moves.urllib.parse.quote(destination) % urllib.parse.quote(destination)
) )
# #
# version pour formulaire avec AJAX (Yann LB) # version pour formulaire avec AJAX (Yann LB)
@ -965,7 +963,7 @@ ou entrez une date pour visualiser les absents un jour donné&nbsp;:
""" """
% (REQUEST.URL0, formsemestre_id, groups_infos.get_form_elem()), % (REQUEST.URL0, formsemestre_id, groups_infos.get_form_elem()),
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
@bp.route("/EtatAbsencesDate") @bp.route("/EtatAbsencesDate")
@ -1101,7 +1099,7 @@ def AddBilletAbsence(
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
tab = _tableBillets(billets, etud=etud) tab = _tableBillets(billets, etud=etud)
log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0)) log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0))
return tab.make_page(REQUEST=REQUEST, format="xml") return tab.make_page(format="xml")
else: else:
return billet_id return billet_id
@ -1232,7 +1230,7 @@ def listeBilletsEtud(etudid=False, REQUEST=None, format="html"):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]}) billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]})
tab = _tableBillets(billets, etud=etud) tab = _tableBillets(billets, etud=etud)
return tab.make_page(REQUEST=REQUEST, format=format) return tab.make_page(format=format)
@bp.route( @bp.route(
@ -1479,20 +1477,24 @@ def ProcessBilletAbsenceForm(billet_id, REQUEST=None):
def XMLgetAbsEtud(beg_date="", end_date="", REQUEST=None): def XMLgetAbsEtud(beg_date="", end_date="", REQUEST=None):
"""returns list of absences in date interval""" """returns list of absences in date interval"""
t0 = time.time() t0 = time.time()
etud = sco_etud.get_etud_info(filled=False)[0] etuds = sco_etud.get_etud_info(filled=False)
if not etuds:
raise APIInvalidParams("étudiant inconnu")
# raise ScoValueError("étudiant inconnu")
etud = etuds[0]
exp = re.compile(r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$") exp = re.compile(r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$")
if not exp.match(beg_date): if not exp.match(beg_date):
raise ScoValueError("invalid date: %s" % beg_date) raise ScoValueError("invalid date: %s" % beg_date)
if not exp.match(end_date): if not exp.match(end_date):
raise ScoValueError("invalid date: %s" % end_date) raise ScoValueError("invalid date: %s" % end_date)
Abs = sco_abs.list_abs_date(etud["etudid"], beg_date, end_date) abs_list = sco_abs.list_abs_date(etud["etudid"], beg_date, end_date)
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
doc = ElementTree.Element( doc = ElementTree.Element(
"absences", etudid=etud["etudid"], beg_date=beg_date, end_date=end_date "absences", etudid=str(etud["etudid"]), beg_date=beg_date, end_date=end_date
) )
for a in Abs: for a in abs_list:
if a["estabs"]: # ne donne pas les justifications si pas d'absence if a["estabs"]: # ne donne pas les justifications si pas d'absence
doc.append( doc.append(
ElementTree.Element( ElementTree.Element(
@ -1500,7 +1502,7 @@ def XMLgetAbsEtud(beg_date="", end_date="", REQUEST=None):
begin=a["begin"], begin=a["begin"],
end=a["end"], end=a["end"],
description=a["description"], description=a["description"],
justified=a["estjust"], justified=str(int(a["estjust"])),
) )
) )
log("XMLgetAbsEtud (%gs)" % (time.time() - t0)) log("XMLgetAbsEtud (%gs)" % (time.time() - t0))

View File

@ -224,7 +224,7 @@ def index_html(REQUEST=None, etud_nom=None, limit=50, offset="", format="html"):
preferences=context.get_preferences(), preferences=context.get_preferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
else: else:
H = [ H = [
entreprise_header(REQUEST=REQUEST, page_title="Suivi entreprises"), entreprise_header(REQUEST=REQUEST, page_title="Suivi entreprises"),
@ -297,7 +297,7 @@ def entreprise_contact_list(entreprise_id=None, format="html", REQUEST=None):
preferences=context.get_preferences(), preferences=context.get_preferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
H.append(tab.html()) H.append(tab.html())
@ -403,7 +403,7 @@ def entreprise_correspondant_list(
preferences=context.get_preferences(), preferences=context.get_preferences(),
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
H.append(tab.html()) H.append(tab.html())

View File

@ -647,7 +647,11 @@ def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None, REQUEST=None):
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
doc = ElementTree.Element("formsemestrelist") doc = ElementTree.Element("formsemestrelist")
for sem in sco_formsemestre.do_formsemestre_list(args=args): for sem in sco_formsemestre.do_formsemestre_list(args=args):
doc.append("formsemestre", **sem) for k in sem:
if isinstance(sem[k], int):
sem[k] = str(sem[k])
sem_elt = ElementTree.Element("formsemestre", **sem)
doc.append(sem_elt)
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
@ -1086,7 +1090,7 @@ def view_module_abs(REQUEST, moduleimpl_id, format="html"):
) )
if format != "html": if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
return "\n".join(H) + tab.html() + html_sco_header.sco_footer() return "\n".join(H) + tab.html() + html_sco_header.sco_footer()
@ -1253,7 +1257,7 @@ def formsemestre_enseignants_list(REQUEST, formsemestre_id, format="html"):
caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.", caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.",
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
) )
return T.make_page(page_title=title, title=title, REQUEST=REQUEST, format=format) return T.make_page(page_title=title, title=title, format=format)
@bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"]) @bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"])

View File

@ -37,6 +37,7 @@ import flask
from flask import abort, flash, url_for, redirect, render_template, send_file from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request from flask import request
from flask.app import Flask from flask.app import Flask
import flask_login
from flask_login.utils import login_required from flask_login.utils import login_required
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed
@ -57,6 +58,7 @@ from app.scodoc import sco_utils as scu
from app.decorators import ( from app.decorators import (
admin_required, admin_required,
scodoc7func, scodoc7func,
scodoc,
permission_required_compat_scodoc7, permission_required_compat_scodoc7,
) )
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -131,6 +133,43 @@ def get_etud_dept():
return Departement.query.get(last_etud.dept_id).acronym return Departement.query.get(last_etud.dept_id).acronym
# Bricolage pour le portail IUTV avec ScoDoc 7: (DEPRECATED: NE PAS UTILISER !)
@bp.route(
"/ScoDoc/search_inscr_etud_by_nip", methods=["GET"]
) # pour compat anciens clients PHP
@scodoc
@scodoc7func
def search_inscr_etud_by_nip(
code_nip, REQUEST=None, format="json", __ac_name="", __ac_password=""
):
auth_ok = False
user_name = __ac_name
user_password = __ac_password
if user_name and user_password:
u = User.query.filter_by(user_name=user_name).first()
if u and u.check_password(user_password):
auth_ok = True
flask_login.login_user(u)
if not auth_ok:
abort(403)
else:
return sco_find_etud.search_inscr_etud_by_nip(
code_nip=code_nip, REQUEST=REQUEST, format=format
)
@bp.route("/ScoDoc/about")
@bp.route("/ScoDoc/Scolarite/<scodoc_dept>/about")
def about(scodoc_dept=None):
"version info"
return render_template(
"about.html",
version=scu.get_scodoc_version(),
news=sco_version.SCONEWS,
logo=scu.icontag("borgne_img"),
)
# ---- CONFIGURATION # ---- CONFIGURATION

View File

@ -131,34 +131,6 @@ def sco_publish(route, function, permission, methods=["GET"]):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@bp.route("/about")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def about():
"version info"
H = [
"""<h2>Système de gestion scolarité</h2>
<p>&copy; Emmanuel Viennet 1997-2021</p>
<p>Version %s</p>
"""
% (scu.get_scodoc_version())
]
H.append(
'<p>Logiciel libre écrit en <a href="http://www.python.org" target="_blank" rel="noopener noreferrer">Python</a>.</p>'
)
H.append("<h2>Dernières évolutions</h2>" + sco_version.SCONEWS)
H.append(
'<div class="about-logo">'
+ scu.icontag("borgne_img")
+ " <em>Au pays des aveugles...</em></div>"
)
d = ""
return (
html_sco_header.sco_header() + "\n".join(H) + d + html_sco_header.sco_footer()
)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# #
# PREFERENCES # PREFERENCES
@ -307,19 +279,10 @@ def showEtudLog(etudid, format="html", REQUEST=None):
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
return tab.make_page(format=format, REQUEST=REQUEST) return tab.make_page(format=format)
# ---------- PAGE ACCUEIL (listes) -------------- # ---------- PAGE ACCUEIL (listes) --------------
# @bp.route("/")
@bp.route("/kimo")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def kimo(REQUEST=None, showcodes=0, showsemtable=0):
import time
return f"{time.time()} := {g.scodoc_dept}"
@bp.route("/") @bp.route("/")

View File

@ -97,11 +97,12 @@ def user_info(user_name, format="json", REQUEST=None):
@scodoc @scodoc
@permission_required(Permission.ScoUsersAdmin) @permission_required(Permission.ScoUsersAdmin)
@scodoc7func @scodoc7func
def create_user_form(REQUEST, user_name=None, edit=0): def create_user_form(REQUEST, user_name=None, edit=0, all_roles=1):
"form. creation ou edit utilisateur" "form. création ou edition utilisateur"
auth_dept = current_user.dept auth_dept = current_user.dept
initvalues = {} initvalues = {}
edit = int(edit) edit = int(edit)
all_roles = int(all_roles)
H = [html_sco_header.sco_header(bodyOnLoad="init_tf_form('')")] H = [html_sco_header.sco_header(bodyOnLoad="init_tf_form('')")]
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
if edit: if edit:
@ -120,11 +121,19 @@ def create_user_form(REQUEST, user_name=None, edit=0):
H.append("""<p class="warning">Vous êtes super administrateur !</p>""") H.append("""<p class="warning">Vous êtes super administrateur !</p>""")
is_super_admin = True is_super_admin = True
# Les rôles standards créés à l'initialisation de ScoDoc: if all_roles:
standard_roles = [ # tous sauf SuperAdmin
Role.get_named_role(r) for r in ("Ens", "Secr", "Admin", "RespPe") standard_roles = [
] r
# Rôles pouvant etre attribués aux utilisateurs via ce dialogue: for r in Role.query.all()
if r.permissions != Permission.ALL_PERMISSIONS[0]
]
else:
# Les rôles standards créés à l'initialisation de ScoDoc:
standard_roles = [
Role.get_named_role(r) for r in ("Ens", "Secr", "Admin", "RespPe")
]
# Départements auxquels ont peut associer des rôles via ce dialogue:
# si SuperAdmin, tous les rôles standards dans tous les départements # si SuperAdmin, tous les rôles standards dans tous les départements
# sinon, les départements dans lesquels l'utilisateur a le droit # sinon, les départements dans lesquels l'utilisateur a le droit
if is_super_admin: if is_super_admin:
@ -209,7 +218,7 @@ def create_user_form(REQUEST, user_name=None, edit=0):
}, },
), ),
( (
"passwd", "password",
{ {
"title": "Mot de passe", "title": "Mot de passe",
"input_type": "password", "input_type": "password",
@ -219,7 +228,7 @@ def create_user_form(REQUEST, user_name=None, edit=0):
}, },
), ),
( (
"passwd2", "password2",
{ {
"title": "Confirmer mot de passe", "title": "Confirmer mot de passe",
"input_type": "password", "input_type": "password",
@ -392,11 +401,11 @@ def create_user_form(REQUEST, user_name=None, edit=0):
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
if edit: # modif utilisateur (mais pas passwd ni user_name !) if edit: # modif utilisateur (mais pas password ni user_name !)
if (not can_choose_dept) and "dept" in vals: if (not can_choose_dept) and "dept" in vals:
del vals["dept"] del vals["dept"]
if "passwd" in vals: if "password" in vals:
del vals["passwd"] del vals["passwordd"]
if "date_modif_passwd" in vals: if "date_modif_passwd" in vals:
del vals["date_modif_passwd"] del vals["date_modif_passwd"]
if "user_name" in vals: if "user_name" in vals:
@ -442,13 +451,13 @@ def create_user_form(REQUEST, user_name=None, edit=0):
) )
return "\n".join(H) + msg + "\n" + tf[1] + F return "\n".join(H) + msg + "\n" + tf[1] + F
# check passwords # check passwords
if vals["passwd"]: if vals["password"]:
if vals["passwd"] != vals["passwd2"]: if vals["password"] != vals["password2"]:
msg = tf_error_message( msg = tf_error_message(
"""Les deux mots de passes ne correspondent pas !""" """Les deux mots de passes ne correspondent pas !"""
) )
return "\n".join(H) + msg + "\n" + tf[1] + F return "\n".join(H) + msg + "\n" + tf[1] + F
if not sco_users.is_valid_password(vals["passwd"]): if not sco_users.is_valid_password(vals["password"]):
msg = tf_error_message( msg = tf_error_message(
"""Mot de passe trop simple, recommencez !""" """Mot de passe trop simple, recommencez !"""
) )

View File

@ -30,6 +30,8 @@ class Config:
SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc") SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc")
SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data") SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data")
SCODOC_LOG_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc.log") SCODOC_LOG_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc.log")
# evite confusion avec le log nginx scodoc_error.log:
SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.log")
# #
MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # Flask uploads MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # Flask uploads
@ -53,7 +55,7 @@ class DevConfig(Config):
DEBUG = True DEBUG = True
TESTING = False TESTING = False
SQLALCHEMY_DATABASE_URI = ( SQLALCHEMY_DATABASE_URI = (
os.environ.get("SCODOC_DEV_DATABASE_URI") or "postgresql:///SCODOC_DEV" os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_DEV"
) )
SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a" SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a"
@ -62,7 +64,7 @@ class TestConfig(DevConfig):
TESTING = True TESTING = True
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = ( SQLALCHEMY_DATABASE_URI = (
os.environ.get("SCODOC_TEST_DATABASE_URI") or "postgresql:///SCODOC_TEST" os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_TEST"
) )
SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "test.gr" SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "test.gr"
DEPT_TEST = "TEST_" # nom du département, ne pas l'utiliser pour un "vrai" DEPT_TEST = "TEST_" # nom du département, ne pas l'utiliser pour un "vrai"

View File

@ -0,0 +1,28 @@
"""Flag bloquage calcul moyennes
Revision ID: 669065fb2d20
Revises: a217bf588f4c
Create Date: 2021-09-16 22:04:11.624632
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '669065fb2d20'
down_revision = 'a217bf588f4c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notes_formsemestre', sa.Column('block_moyennes', sa.Boolean(), server_default='false', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('notes_formsemestre', 'block_moyennes')
# ### end Alembic commands ###

View File

@ -1,68 +1,69 @@
Notes sur la restauration de la base SQL complete Notes sur la restauration de la base SQL complete (ScoDoc 9)
(dans le cas d'une réinstallation sur une autre machine, par exemple) (dans le cas d'une réinstallation sur une autre machine, par exemple)
1) Sur la machine origine, faire un dump complet: 1) Sur la machine origine, faire un dump complet:
su postgres su postgres
cd /tmp # ou ailleurs... cd /tmp # ou ailleurs...
pg_dumpall > scodoc.dump.txt pg_dumpall | gzip > scodoc.dump.txt.gz
On obtient un fichier texte assez volumineux (on peut utiliser gzip pour le compresser avant transfert). On obtient un fichier texte assez volumineux (comprimé par gzip ci-dessus)
Le copier sur la machine destination. Le copier sur la machine destination, et le décompresser (gunzip).
2) Sur la machine destination: 2) Sur la machine destination:
Avant toute chose, stopper scodoc: Avant toute chose, stopper scodoc:
/etc/init.d/scodoc stop (ou systemctl stop scodoc))
systemctl stop scodoc9
1.1) Supprimer toutes les bases ScoDoc existantes s'il y en a: 1.1) Supprimer toutes les bases ScoDoc existantes s'il y en a:
su postgres su scodoc
psql -l psql -l
liste les bases: celles de ScoDoc sont SCO* liste les bases: celles de ScoDoc sont SCO*
Pour chaque base SCO*, faire dropdb Pour chaque base SCO*, faire dropdb
dropdb SCOUSERS dropdb SCODOC
dropdb SCOGEII dropdb SCODOC_DEV
... ...
Pour les feignants, voici un script (à lancer comme utilisateur postgres): Pour les gens pressés, voici un script (à lancer comme utilisateur postgres):
for f in $(psql -l --no-align --field-separator . | grep SCO | cut -f 1 -d.); do for f in $(psql -l --no-align --field-separator . | grep SCO | cut -f 1 -d.); do
echo dropping $f echo dropping $f
dropdb $f dropdb $f
done done
1.2) Charger le dump (toujours comme utilisateur postgres): 1.2) Charger le dump (toujours comme utilisateur scodoc):
psql -f scodoc.dump.txt postgres psql -f scodoc.dump.txt scodoc
1.3) Recopier les fichiers (photos, config, archives): copier le repertoire complet 1.3) Recopier les fichiers (photos, config, archives): copier le repertoire complet
/opt/scodoc/instance/var /opt/scodoc-data
de la machine origine vers la nouvelle de la machine origine vers la nouvelle
Puis redemarrer ScoDoc: Puis redemarrer ScoDoc:
en tant que root: /etc/init.d/scodoc start (ou systemctl start scodoc) en tant que root: systemctl start scodoc9
NB: si la version des sources a changée, lancer imperativement le script de mise a jour NB: si la version des sources a changée, lancer imperativement le script de mise a jour
avant de redemarrer scodoc, afin qu'il change si besoin la base de donnees: avant de redemarrer scodoc, afin qu'il change si besoin la base de donnees:
(en tant que root): (en tant que root):
cd /opt/scodoc/instance/Products/ScoDoc/config apt-get update &&
./upgrade.sh
---- ----
Cas d'une seule base à copier: (eg un seul département, mais faire Cas d'une seule base à copier (base production ou dev. par exemple)
attention aux utilisateurs definis dans la base SCOUSERS):
En tant qu'utilisateur "postgres": En tant qu'utilisateur "scodoc":
Dump: (script avec commande de creation de la base) Dump: permettant de la recharger en changeant son nom
pg_dump --create SCOINFO > /tmp/scoinfo.dump pg_dump --format=custom --file=/tmp/SCODOC.dump SCODOC
Restore: (si necessaire, utiliser dropdb avant) Restore: (si necessaire, utiliser dropdb avant)
psql -f /tmp/scoinfo.dump postgres createdb SCODOC_IUTV
pg_restore -d SCODOC_IUTV SCODOC.dump
(si on veut garder le même nom de base que l'origine, utiliser --create )
--- --- à revoir
Cas d'un dump via sco_dump_db (assistance): Cas d'un dump via sco_dump_db (assistance):
createdb -E UTF-8 SCOXXX createdb -E UTF-8 SCOXXX
zcat xxx | psql SCOXXX zcat xxx | psql SCOXXX

View File

@ -1,5 +1,5 @@
-- Creation des tables pour gestion notes -- Creation des tables pour gestion notes ScoDoc 7 (OBSOLETE !)
-- E. Viennet, Sep 2005 -- E. Viennet, Sep 2005

View File

@ -1,14 +1,14 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.0.13" SCOVERSION = "9.0.28"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2021</h4> <h4>Année 2021</h4>
<ul> <ul>
<li>ScoDoc 9: nouvelle architecture logicielle</li> <li>ScoDoc 9: nouvelle architecture logicielle (Flask/Python3/Debian 11)</li>
<li>Version mobile (en test)</li> <li>Version mobile (en test)</li>
<li>Évaluations de type "deuxième session"</li> <li>Évaluations de type "deuxième session"</li>
<li>Gestion du genre neutre (pas d'affichage de la civilité)</li> <li>Gestion du genre neutre (pas d'affichage de la civilité)</li>

120
scodoc.py
View File

@ -6,24 +6,22 @@
""" """
from __future__ import print_function
import os
from pprint import pprint as pp from pprint import pprint as pp
import re
import sys import sys
import click import click
import flask import flask
from flask.cli import with_appcontext from flask.cli import with_appcontext
from app import create_app, cli, db from app import create_app, cli, db
from app import initialize_scodoc_database from app import initialize_scodoc_database
from app import clear_scodoc_cache from app import clear_scodoc_cache
from app import models
from app.auth.models import User, Role, UserRole from app.auth.models import User, Role, UserRole
from app import models
from app.models import ScoPreference from app.models import ScoPreference
from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar, absences from app.views import notes, scolar, absences
import tools import tools
@ -45,6 +43,7 @@ def make_shell_context():
"User": User, "User": User,
"Role": Role, "Role": Role,
"UserRole": UserRole, "UserRole": UserRole,
"Permission": Permission,
"notes": notes, "notes": notes,
"scolar": scolar, "scolar": scolar,
"ndb": ndb, "ndb": ndb,
@ -142,13 +141,90 @@ def user_password(username, password=None): # user-password
return 1 return 1
u = User.query.filter_by(user_name=username).first() u = User.query.filter_by(user_name=username).first()
if not u: if not u:
sys.stderr.write("user_password: user {} does not exists".format(username)) sys.stderr.write(f"user_password: user {username} does not exists\n")
return 1 return 1
u.set_password(password) u.set_password(password)
db.session.add(u) db.session.add(u)
db.session.commit() db.session.commit()
click.echo("changed password for user {}".format(u)) click.echo(f"changed password for user {u}")
@app.cli.command()
@click.argument("rolename")
@click.argument("permissions", nargs=-1)
def create_role(rolename, permissions): # create-role
"""Create a new role"""
# Check rolename
if not re.match(r"^[a-zA-Z0-9]+$", rolename):
sys.stderr.write(f"create_role: invalid rolename {rolename}\n")
return 1
# Check permissions
permission_list = []
for permission_name in permissions:
perm = Permission.get_by_name(permission_name)
if not perm:
sys.stderr.write(f"create_role: invalid permission name {perm}\n")
sys.stderr.write(
f"\tavailable permissions: {', '.join([ name for name in Permission.permission_by_name])}.\n"
)
return 1
permission_list.append(perm)
role = Role.query.filter_by(name=rolename).first()
if role:
sys.stderr.write(f"create_role: role {rolename} already exists\n")
return 1
role = Role(name=rolename)
for perm in permission_list:
role.add_permission(perm)
db.session.add(role)
db.session.commit()
@app.cli.command()
@click.argument("rolename")
@click.option("-a", "--add", "addpermissionname")
@click.option("-r", "--remove", "removepermissionname")
def edit_role(rolename, addpermissionname=None, removepermissionname=None): # edit-role
"""Add [-a] and/or remove [-r] a permission to/from a role.
In ScoDoc, permissions are not associated to users but to roles.
Each user has a set of roles in each departement.
Example: `flask edit-role -a ScoEditApo Ens`
"""
if addpermissionname:
perm_to_add = Permission.get_by_name(addpermissionname)
if not perm_to_add:
sys.stderr.write(
f"edit_role: permission {addpermissionname} does not exists\n"
)
return 1
else:
perm_to_add = None
if removepermissionname:
perm_to_remove = Permission.get_by_name(removepermissionname)
if not perm_to_remove:
sys.stderr.write(
f"edit_role: permission {removepermissionname} does not exists\n"
)
return 1
else:
perm_to_remove = None
role = Role.query.filter_by(name=rolename).first()
if not role:
sys.stderr.write(f"edit_role: role {rolename} does not exists\n")
return 1
if perm_to_add:
role.add_permission(perm_to_add)
click.echo(f"adding permission {addpermissionname} to role {rolename}")
if perm_to_remove:
role.remove_permission(perm_to_remove)
click.echo(f"removing permission {removepermissionname} from role {rolename}")
if perm_to_add or perm_to_remove:
db.session.add(role)
db.session.commit()
@app.cli.command() @app.cli.command()
@ -191,7 +267,7 @@ def create_dept(dept): # create-dept
@app.cli.command() @app.cli.command()
@with_appcontext @with_appcontext
def import_scodoc7_users(): # import-scodoc7-users def import_scodoc7_users(): # import-scodoc7-users
"""Import used defined in ScoDoc7 postgresql database into ScoDoc 9 """Import users defined in ScoDoc7 postgresql database into ScoDoc 9
The old database SCOUSERS must be alive and readable by the current user. The old database SCOUSERS must be alive and readable by the current user.
This script is typically run as unix user "scodoc". This script is typically run as unix user "scodoc".
The original SCOUSERS database is left unmodified. The original SCOUSERS database is left unmodified.
@ -206,18 +282,40 @@ def import_scodoc7_users(): # import-scodoc7-users
@click.argument("dept") @click.argument("dept")
@click.argument("dept_db_name") @click.argument("dept_db_name")
@with_appcontext @with_appcontext
def import_scodoc7_dept(dept: str, dept_db_name: str): # import-scodoc7-dept def import_scodoc7_dept(dept: str, dept_db_name: str = ""): # import-scodoc7-dept
"""Import département ScoDoc 7: dept: InfoComm, dept_db_name: SCOINFOCOMM""" """Import département ScoDoc 7: dept: InfoComm, dept_db_name: SCOINFOCOMM"""
dept_db_uri = f"postgresql:///{dept_db_name}" dept_db_uri = f"postgresql:///{dept_db_name}"
tools.import_scodoc7_dept(dept, dept_db_uri) tools.import_scodoc7_dept(dept, dept_db_uri)
@app.cli.command()
@click.argument("dept", default="")
@with_appcontext
def migrate_scodoc7_dept_archive(dept: str): # migrate-scodoc7-dept-archive
"""Post-migration: renomme les archives en fonction des id de ScoDoc 9"""
tools.migrate_scodoc7_dept_archive(dept)
@app.cli.command() @app.cli.command()
@with_appcontext @with_appcontext
def clear_cache(): # clear-cache def clear_cache(): # clear-cache
"""Clear ScoDoc cache """Clear ScoDoc cache
This cache (currently Redis) is persistent between invocation This cache (currently Redis) is persistent between invocation
and it may be necessary to clear it during developement or tests. and it may be necessary to clear it during development or tests.
""" """
clear_scodoc_cache() clear_scodoc_cache()
click.echo("Redis caches flushed.") click.echo("Redis caches flushed.")
def recursive_help(cmd, parent=None):
ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
print(cmd.get_help(ctx))
print()
commands = getattr(cmd, "commands", {})
for sub in commands.values():
recursive_help(sub, ctx)
@app.cli.command()
def dumphelp():
recursive_help(app.cli)

View File

@ -219,6 +219,7 @@ class ScoFake(object):
etat=None, etat=None,
gestion_compensation=None, gestion_compensation=None,
bul_hide_xml=None, bul_hide_xml=None,
block_moyennes=None,
gestion_semestrielle=None, gestion_semestrielle=None,
bul_bgcolor=None, bul_bgcolor=None,
modalite=NotesFormModalite.DEFAULT_MODALITE, modalite=NotesFormModalite.DEFAULT_MODALITE,

View File

@ -6,3 +6,4 @@
from tools.import_scodoc7_user_db import import_scodoc7_user_db from tools.import_scodoc7_user_db import import_scodoc7_user_db
from tools.import_scodoc7_dept import import_scodoc7_dept from tools.import_scodoc7_dept import import_scodoc7_dept
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archive

View File

@ -1,29 +1,28 @@
#!/bin/bash #!/bin/bash
# usage: backup_db2 dbname # usage: backup_db9 dbname
# Dump une base postgresql, et garde plusieurs dumps dans le passe # Dump une base postgresql, et garde plusieurs dumps dans le passe
# (configurable dans le script backup_rotation.sh) # (configurable dans le script backup_rotation.sh)
# Les dumps sont compresses (gzip). # Les dumps sont compresses (gzip).
# #
# E. Viennet pour ScoDoc, 2014 # E. Viennet pour ScoDoc, 2014, 2021 pour ScoDoc 9
# (ce script est meilleur que l'ancien backup-db, car l'espace entre # (ce script est meilleur que l'ancien backup-db, car l'espace entre
# deux sauvegardes dépend de leur anciennete) # deux sauvegardes dépend de leur anciennete)
# #
# #
# Note: pour restaurer un backup (en supprimant la base existante !): # Note: pour restaurer un backup (en supprimant la base existante !):
# #
# 0- Arreter scodoc: /etc/init.d/scodoc stop (ou systemctl stop scodoc) # 0- Arreter scodoc: systemctl stop scodoc
# #
# Puis en tant qu'utilisateur postgres: su postgres # Puis en tant qu'utilisateur scodoc: su scodoc
# 1- supprimer la base existante si elle existe: dropdb SCOXXX # 1- supprimer la base existante si elle existe: dropdb SCODOC
# #
# 2- recreer la base, vide: createdb -E UTF-8 SCOXXX # 2- recreer la base, vide: createdb -E UTF-8 SCOXXX
# (nom de la base: SCOXXX ou XXX=departement) # /opt/scodoc/tools/create_database.sh SCODOC
# # 3- pg_restore -d SCODOC SCODOC_pgdump
# 3- pg_restore -d SCOXXX SCOXXX_pgdump
# #
# Revenir a l'utilisateur root: exit # Revenir a l'utilisateur root: exit
# 4- Relancer scodoc: /etc/init.d/scodoc start (ou systemctl start scodoc) # 4- Relancer scodoc: systemctl start scodoc
DBNAME=$1 DBNAME=$1
DUMPBASE="$DBNAME"-BACKUPS DUMPBASE="$DBNAME"-BACKUPS
@ -50,4 +49,4 @@ pg_dump --format=t "$DBNAME" -f $DUMPFILE
gzip $DUMPFILE gzip $DUMPFILE
# 3- Rotate backups: remove unneeded copies # 3- Rotate backups: remove unneeded copies
/opt/scodoc/Products/ScoDoc/misc/backup_rotation.sh "$DUMPBASE" /opt/scodoc/tools/backups/backup_rotation.sh "$DUMPBASE"

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Backup rotation # Backup rotation
# Usage example: backup_rotation.sh /var/lib/postgresql/BACKUP-SCOGEII # Usage example: backup_rotation.sh /var/lib/postgresql/BACKUP-SCODOC
# #
# This script is designed to run each hour # This script is designed to run each hour
# #

View File

@ -11,7 +11,7 @@
# #
# A adapter a vos besoins. Utilisation a vos risques et perils. # A adapter a vos besoins. Utilisation a vos risques et perils.
# #
# E. Viennet, 2002 # E. Viennet, 2002, 2021
# Installation: # Installation:
# 1- Installer rsync: # 1- Installer rsync:
@ -30,8 +30,8 @@ logfile=/var/log/rsynclog # log sur serveur scodoc
# A qui envoyer un mail en cas d'erreur de la sauvegarde: # A qui envoyer un mail en cas d'erreur de la sauvegarde:
SUPERVISORMAIL=emmanuel.viennet@example.com SUPERVISORMAIL=emmanuel.viennet@example.com
CALLER=`basename $0` CALLER=$(basename $0)
MACHINE=`hostname -s` MACHINE=$(hostname -s)
# ----------------------------------------------------- # -----------------------------------------------------
@ -40,7 +40,7 @@ MACHINE=`hostname -s`
# ---------------------------------- # ----------------------------------
terminate() terminate()
{ {
dateTest=`date` dateTest=$(date)
mail -s "Attention: Probleme sauvegarde ScoDoc" $SUPERVISORMAIL <<EOF mail -s "Attention: Probleme sauvegarde ScoDoc" $SUPERVISORMAIL <<EOF
The execution of script $CALLER was not successful on $MACHINE. The execution of script $CALLER was not successful on $MACHINE.
@ -57,7 +57,7 @@ EOF
echo "Look at logfile $logfile" echo "Look at logfile $logfile"
echo echo
echo "$CALLER terminated, exiting now with rc=1." echo "$CALLER terminated, exiting now with rc=1."
dateTest=`date` dateTest=$(date)
echo "End of script at: $dateTest" echo "End of script at: $dateTest"
echo "" echo ""
@ -74,16 +74,16 @@ EOF
# -------------------------------------- # --------------------------------------
rsync_mirror_to_remote() rsync_mirror_to_remote()
{ {
echo "--------------- mirroring " $MACHINE:$srcdir " to " $remotehost:$destdir >> $logfile 2>&1 echo "--------------- mirroring $MACHINE:$srcdir to $remotehost:$destdir" >> $logfile 2>&1
echo "starting at" `date` >> $logfile 2>&1 echo "starting at $(date)" >> $logfile 2>&1
rsync -vaze ssh --delete --rsync-path=/usr/bin/rsync $srcdir $remotehost":"$destdir >> $logfile 2>&1 rsync -vaze ssh --delete --rsync-path=/usr/bin/rsync "$srcdir" "$remotehost:$destdir" >> $logfile 2>&1
if [ $? -ne 0 ] if [ $? -ne 0 ]
then then
echo Error in rsync: code=$? echo Error in rsync: code=$?
terminate terminate
fi fi
echo "ending at" `date` >> $logfile 2>&1 echo "ending at $(date)" >> $logfile 2>&1
echo "---------------" >> $logfile 2>&1 echo "---------------" >> $logfile 2>&1
} }

View File

@ -15,9 +15,11 @@ source "$SCRIPT_DIR/utils.sh"
SCODOC_RELEASE=$(grep SCOVERSION "$SCRIPT_DIR/../sco_version.py" | awk '{ print substr($3, 2, length($3)-2) }') SCODOC_RELEASE=$(grep SCOVERSION "$SCRIPT_DIR/../sco_version.py" | awk '{ print substr($3, 2, length($3)-2) }')
# Dernière release # Dernière release
GITEA_RELEASE_URL="https://scodoc.org/git/api/v1/repos/viennet/ScoDoc/releases?pre-release=true" GITEA_RELEASE_URL="https://scodoc.org/git/api/v1/repos/viennet/ScoDoc/releases" # ?pre-release=true"
LAST_RELEASE_TAG=$(curl "$GITEA_RELEASE_URL" | jq ".[].tag_name" | sort | tail -1 | awk '{ print substr($1, 2, length($1)-2) }') # suppose que les realse sont nommées 9.0.17, ne considère pas les caractères non numériques
LAST_RELEASE_TAG=$(curl "$GITEA_RELEASE_URL" | jq ".[].tag_name" | tr -d -c "0-9.\n" | sort --version-sort | tail -1)
# | awk '{ print substr($1, 2, length($1)-2) }')
echo echo
echo "Version détectée dans le source: $SCODOC_RELEASE" echo "Version détectée dans le source: $SCODOC_RELEASE"
@ -89,7 +91,8 @@ fi
# Puis on déplace les fichiers de config (nginx, systemd, ...) # Puis on déplace les fichiers de config (nginx, systemd, ...)
# nginx: # nginx:
mkdir -p "$slash"/etc/nginx/sites-available || die "can't mkdir nginx config" mkdir -p "$slash"/etc/nginx/sites-available || die "can't mkdir nginx config"
cp -p "$SCODOC_DIR"/tools/etc/scodoc9.nginx "$slash"/etc/nginx/sites-available/ || die "can't copy nginx config" cp -p "$SCODOC_DIR"/tools/etc/scodoc9.nginx "$slash"/etc/nginx/sites-available/scodoc9.nginx.distrib || die "can't copy nginx config"
# systemd # systemd
mkdir -p "$slash"/etc/systemd/system/ || die "can't mkdir systemd config" mkdir -p "$slash"/etc/systemd/system/ || die "can't mkdir systemd config"
cp -p "$SCODOC_DIR"/tools/etc/scodoc9.service "$slash"/etc/systemd/system/ || die "can't copy scodoc9.service" cp -p "$SCODOC_DIR"/tools/etc/scodoc9.service "$slash"/etc/systemd/system/ || die "can't copy scodoc9.service"

View File

@ -80,6 +80,11 @@ su -c "(cd $SCODOC_DIR && python3 -m venv venv)" "$SCODOC_USER" || die "Error cr
su -c "(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt)" "$SCODOC_USER" || die "Error installing python packages" su -c "(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt)" "$SCODOC_USER" || die "Error installing python packages"
# --- NGINX # --- NGINX
# Evite d'écraser: il faudrait ici présenter un dialogue "fichier local modifié, ..."
if [ ! -e /etc/nginx/sites-available/scodoc9.nginx ]
then
cp -p /etc/nginx/sites-available/scodoc9.nginx.distrib /etc/nginx/sites-available/scodoc9.nginx || die "can't copy nginx config"
fi
if [ ! -L /etc/nginx/sites-enabled/scodoc9.nginx ] if [ ! -L /etc/nginx/sites-enabled/scodoc9.nginx ]
then then
echo "Enabling scodoc9 in nginx" echo "Enabling scodoc9 in nginx"

View File

@ -1,4 +1,13 @@
/opt/scodoc-datalog/scodoc.log { /opt/scodoc-data/log/scodoc.log {
weekly
missingok
rotate 64
compress
notifempty
dateext
create 0644 scodoc scodoc
}
/opt/scodoc-datalog/scodoc_exc.log {
weekly weekly
missingok missingok
rotate 64 rotate 64

View File

@ -27,6 +27,7 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
} }
location /ScoDoc/static { location /ScoDoc/static {
# handle static files directly, without forwarding to the application # handle static files directly, without forwarding to the application
@ -40,5 +41,8 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /favicon.ico {
alias /opt/scodoc/app/static/icons/favicon.ico;
}
} }

View File

@ -380,6 +380,9 @@ def convert_object(
"absences", "absences",
"absences_notifications", "absences_notifications",
"itemsuivi", # etudid n'était pas une clé "itemsuivi", # etudid n'était pas une clé
"adresse", # etudid n'était pas une clé
"admissions", # idem
"scolar_events",
}: }:
# tables avec "fausses" clés # tables avec "fausses" clés
# (l'object référencé a pu disparaitre) # (l'object référencé a pu disparaitre)

View File

@ -204,6 +204,9 @@ do
systemctl restart postgresql systemctl restart postgresql
done done
# ----- Post-Migration: renomme archives en fonction des nouveaux ids
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask migrate-scodoc7-dept-archive)" "$SCODOC_USER" || die "Erreur de la post-migration des archives"
# --- Si migration "en place", désactive ScoDoc 7 # --- Si migration "en place", désactive ScoDoc 7
if [ "$INPLACE" == 1 ] if [ "$INPLACE" == 1 ]

View File

@ -0,0 +1,72 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
import glob
import os
import shutil
from app.models import Departement
from app.models.formsemestre import FormSemestre
from app.models.etudiants import Identite
def migrate_scodoc7_dept_archive(dept_name=""):
if dept_name:
depts = Departement.query.filter_by(acronym=dept_name)
else:
depts = Departement.query
for dept in depts:
print(f"Migrating {dept.acronym} archives...")
# SemsArchiver
# /opt/scodoc-data/archives/<dept>/<scodoc7id> -> formsemestre_id
migrate_sem_archives(dept)
# EtudsArchiver:
migrate_docetuds(dept)
# ApoCSVArchiver:
# /opt/scodoc-data/archives/apo_csv/<dept>/ ne bouge pas
def migrate_sem_archives(dept):
"/opt/scodoc-data/archives/<dept>/<scodoc7id> -> formsemestre_id"
n = 0
n_moves = 0
for sem in FormSemestre.query.filter_by(dept_id=dept.id):
n += 1
arch_dir7 = f"/opt/scodoc-data/archives/{dept.acronym}/{sem.scodoc7_id}"
arch_dir9 = f"/opt/scodoc-data/archives/{dept.acronym}/{sem.id}"
if os.path.exists(arch_dir7):
n_moves += 1
if not os.path.exists(arch_dir9):
# print(f"renaming {arch_dir7} to {arch_dir9}")
shutil.move(arch_dir7, arch_dir9)
else:
# print(f"merging {arch_dir7} with {arch_dir9}")
for arch in glob.glob(f"{arch_dir7}/*"):
# print(f"\tmoving {arch}")
shutil.move(arch, arch_dir9)
# print(f"moved {n_moves}/{n} sems")
def migrate_docetuds(dept):
"/opt/scodoc-data/archives/docetuds/<dept>/<scodoc7_id>/ -> etudid"
n = 0
n_moves = 0
for etud in Identite.query.filter_by(dept_id=dept.id):
n += 1
arch_dir7 = (
f"/opt/scodoc-data/archives/docetuds/{dept.acronym}/{etud.scodoc7_id}"
)
arch_dir9 = f"/opt/scodoc-data/archives/docetuds/{dept.acronym}/{etud.id}"
if os.path.exists(arch_dir7):
n_moves += 1
if not os.path.exists(arch_dir9):
# print(f"renaming {arch_dir7} to {arch_dir9}")
shutil.move(arch_dir7, arch_dir9)
else:
# print(f"merging {arch_dir7} with {arch_dir9}")
for arch in glob.glob(f"{arch_dir7}/*"):
# print(f"\tmoving {arch}")
shutil.move(arch, arch_dir9)
# print(f"moved {n_moves}/{n} etuds")

View File

@ -44,7 +44,7 @@ fi
# Safety check # Safety check
echo "Ce script recharge les donnees de votre installation ScoDoc 7" echo "Ce script recharge les donnees de votre installation ScoDoc 7"
echo "sur ce serveur pour migration vers ScoDoc 9." echo "sur ce serveur pour migration vers ScoDoc 9."
echo "Ce fichier doit avoir ete cree par le script save_scodoc_data.sh, sur une machine ScoDoc 7." echo "Ce fichier doit avoir ete cree par le script save_scodoc7_data.sh, sur une machine ScoDoc 7."
echo echo
echo -n "Voulez-vous poursuivre cette operation ? (y/n) [n]" echo -n "Voulez-vous poursuivre cette operation ? (y/n) [n]"
read -r ans read -r ans