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-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
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
`tools/build_release.sh`.

View File

@ -2,6 +2,7 @@
# pylint: disable=invalid-name
import os
import re
import socket
import sys
import time
@ -12,19 +13,19 @@ from logging.handlers import SMTPHandler, WatchedFileHandler
from flask import current_app, g, request
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.logging import default_handler
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_login import LoginManager, current_user
from flask_mail import Mail
from flask_bootstrap import Bootstrap
from flask_moment import Moment
from flask_caching import Cache
import sqlalchemy
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams
from config import DevConfig
import sco_version
@ -32,7 +33,7 @@ db = SQLAlchemy()
migrate = Migrate(compare_type=True)
login = LoginManager()
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()
bootstrap = Bootstrap()
moment = Moment()
@ -56,6 +57,12 @@ def internal_server_error(e):
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:
"""Load and render an HTML file _without_ using Flask
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
class RequestFormatter(logging.Formatter):
class LogRequestFormatter(logging.Formatter):
"""Ajoute URL et remote_addr for logging"""
def format(self, record):
@ -86,12 +93,64 @@ class RequestFormatter(logging.Formatter):
else:
record.url = None
record.remote_addr = None
record.sco_user = current_user
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):
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
app.wsgi_app = ReverseProxied(app.wsgi_app)
app.logger.setLevel(logging.DEBUG)
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(500, internal_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
@ -132,9 +192,16 @@ def create_app(config_class=DevConfig):
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
)
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
scodoc_exc_formatter = RequestFormatter(
"[%(asctime)s] %(remote_addr)s requested %(url)s\n"
"%(levelname)s in %(module)s: %(message)s"
scodoc_log_formatter = LogRequestFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(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.debug:
@ -150,11 +217,11 @@ def create_app(config_class=DevConfig):
if app.config["MAIL_USE_TLS"]:
secure = ()
host_name = socket.gethostname()
mail_handler = SMTPHandler(
mail_handler = ScoSMTPHandler(
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
fromaddr="no-reply@" + app.config["MAIL_SERVER"],
toaddrs=["exception@scodoc.org"],
subject="ScoDoc Exception from " + host_name,
subject="ScoDoc Exception", # unused see ScoSMTPHandler
credentials=auth,
secure=secure,
)
@ -163,7 +230,7 @@ def create_app(config_class=DevConfig):
app.logger.addHandler(mail_handler)
else:
# Pour logs en DEV uniquement:
default_handler.setFormatter(scodoc_exc_formatter)
default_handler.setFormatter(scodoc_log_formatter)
# Config logs pour DEV et PRODUCTION
# Configuration des logs (actifs aussi en mode development)
@ -172,9 +239,17 @@ def create_app(config_class=DevConfig):
file_handler = WatchedFileHandler(
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)
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.info(f"{sco_version.SCONAME} {sco_version.SCOVERSION} startup")
@ -353,4 +428,4 @@ from app.scodoc import sco_cache
# click.echo(
# "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):
user_name = StringField(_l("Username"), validators=[DataRequired()])
password = PasswordField(_l("Password"), validators=[DataRequired()])
remember_me = BooleanField(_l("Remember Me"))
submit = SubmitField(_l("Sign In"))
user_name = StringField(_l("Nom d'utilisateur"), validators=[DataRequired()])
password = PasswordField(_l("Mot de passe"), validators=[DataRequired()])
remember_me = BooleanField(_l("mémoriser la connexion"))
submit = SubmitField(_l("Suivant"))
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()])
password = PasswordField(_l("Password"), validators=[DataRequired()])
password = PasswordField(_l("Mot de passe"), validators=[DataRequired()])
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):
user = User.query.filter_by(user_name=user_name.data).first()
@ -48,9 +48,9 @@ class ResetPasswordRequestForm(FlaskForm):
class ResetPasswordForm(FlaskForm):
password = PasswordField(_l("Password"), validators=[DataRequired()])
password = PasswordField(_l("Mot de passe"), validators=[DataRequired()])
password2 = PasswordField(
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")]
_l("Répéter"), validators=[DataRequired(), EqualTo("password")]
)
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()
if user is None or not user.check_password(form.password.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"))
login_user(user, remember=form.remember_me.data)
current_app.logger.info("login: success (%s)", form.user_name.data)
@ -95,7 +95,7 @@ def reset_password_request():
current_app.logger.info(
"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 render_template(
"auth/reset_password_request.html", title=_("Reset Password"), form=form

View File

@ -43,12 +43,14 @@ class ZRequest(object):
"Emulating Zope 2 REQUEST"
def __init__(self):
if current_app.config["DEBUG"]:
self.URL = request.base_url
self.BASE0 = request.url_root
else:
self.URL = request.base_url.replace("http://", "https://")
self.BASE0 = request.url_root.replace("http://", "https://")
# if current_app.config["DEBUG"]:
# le ReverseProxied se charge maintenant de mettre le bon protocole http ou https
self.URL = request.base_url
self.BASE0 = request.url_root
# else:
# self.URL = request.base_url.replace("http://", "https://")
# self.BASE0 = request.url_root.replace("http://", "https://")
self.URL0 = self.URL
# query_string is bytes:
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_ine = db.Column(db.Text())
# 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)

View File

@ -19,7 +19,7 @@ class FormSemestre(db.Model):
id = db.Column(db.Integer, primary_key=True)
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
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
@ -41,6 +41,10 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column(
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):
gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
@ -70,6 +74,7 @@ class FormSemestre(db.Model):
"NotesFormsemestreEtape", cascade="all,delete", backref="notes_formsemestre"
)
# 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)
def __init__(self, **kwargs):

View File

@ -8,6 +8,7 @@
v 1.3 (python3)
"""
import html
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]):
R.append('<span class="tf-ro-value">%s</span>' % labels[i])
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":
pass
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"?>
<!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">
@ -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/menu.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/sorttable.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript">
<script src="/ScoDoc/static/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/sorttable.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script>
<script>
window.onload=function(){enableTooltips("gtrcontent")};
</script>
<script language="javascript" type="text/javascript" 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 language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.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" />
<script language="javascript" type="text/javascript" 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/scodoc.js"></script>
<script src="/ScoDoc/static/js/etud_info.js"></script>
"""
def scodoc_top_html_header(page_title="ScoDoc: bienvenue"):
H = [
_HTML_BEGIN % {"page_title": page_title, "encoding": scu.SCO_ENCODING},
_TOP_LEVEL_CSS,
"""</head><body class="gtrcontent" id="gtrcontent">""",
scu.CUSTOM_HTML_HEADER_CNX,
]
@ -185,13 +180,10 @@ def sco_header(
init_jquery = True
H = [
"""<?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">
<html xmlns="http://www.w3.org/1999/xhtml">
"""<!DOCTYPE html><html lang="fr">
<head>
<meta charset="utf-8"/>
<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="DESCRIPTION" content="ScoDoc" />
@ -206,9 +198,7 @@ def sco_header(
)
if init_google_maps:
# It may be necessary to add an API key:
H.append(
'<script type="text/javascript" src="https://maps.google.com/maps/api/js"></script>'
)
H.append('<script src="https://maps.google.com/maps/api/js"></script>')
# Feuilles de style additionnelles:
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/gt_table.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/menu.js"></script>
<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/bubble.js"></script>
<script type="text/javascript">
<script src="/ScoDoc/static/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script>
<script>
window.onload=function(){enableTooltips("gtrcontent")};
var SCO_URL="%(ScoURL)s";
@ -236,16 +226,14 @@ def sco_header(
# jQuery
if init_jquery:
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(
'<script language="javascript" type="text/javascript" src="/ScoDoc/static/libjs/jquery.field.min.js"></script>'
)
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
# qTip
if init_qtip:
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(
'<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:
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>'
)
# 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>'
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.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:
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:
H.append(
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
)
H.append(
'<script type="text/javascript" src="/ScoDoc/static/DataTables/datatables.min.js"></script>'
)
H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
# JS additionels
for js in javascripts:
H.append(
"""<script language="javascript" type="text/javascript" src="/ScoDoc/static/%s"></script>\n"""
% js
)
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
H.append(
"""<style type="text/css">
"""<style>
.gtrcontent {
margin-left: %(margin_left)s;
height: 100%%;
@ -290,7 +271,7 @@ def sco_header(
)
# Scripts de la page:
if scripts:
H.append("""<script language="javascript" type="text/javascript">""")
H.append("""<script>""")
for script in scripts:
H.append(script)
H.append("""</script>""")

View File

@ -28,9 +28,8 @@
"""
Génération de la "sidebar" (marge gauche des pages HTML)
"""
from flask import url_for
from flask import g
from flask import request
from flask import render_template, url_for
from flask import g, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
@ -152,11 +151,12 @@ def sidebar():
# Logo
H.append(
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>
</div></div>
<div class="logo-logo"><a href= { url_for( 'scolar.about', scodoc_dept=g.scodoc_dept ) }
">{ scu.icontag("scologo_img", no_size=True) }</a>
<div class="logo-logo">
<a href="{ url_for( 'scodoc.about', scodoc_dept=g.scodoc_dept ) }">
{ scu.icontag("scologo_img", no_size=True) }</a>
</div>
</div>
<!-- end of sidebar -->
@ -167,19 +167,7 @@ def sidebar():
def sidebar_dept():
"""Partie supérieure de la marge de gauche"""
H = [
f"""<h2 class="insidebar">Dépt. {sco_preferences.get_preference("DeptName")}</h2>
<a href="{url_for("scodoc.index")}" class="sidebar">Accueil</a> <br/> """
]
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)
return render_template(
"sidebar_dept.html",
prefs=sco_preferences.SemPreferences(),
)

View File

@ -186,6 +186,8 @@ class NotesTable(object):
self.use_ue_coefs = sco_preferences.get_preference(
"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
self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
@ -738,6 +740,7 @@ class NotesTable(object):
block_computation = (
self.inscrdict[etudid]["etat"] == "D"
or self.inscrdict[etudid]["etat"] == DEF
or self.block_moyennes
)
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()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""
SELECT DISTINCT A.JOUR, A.MATIN
FROM ABSENCES A
WHERE A.ETUDID = %(etudid)s
AND A.ESTABS"""
"""SELECT DISTINCT A.JOUR, A.MATIN
FROM ABSENCES A
WHERE A.ETUDID = %(etudid)s
AND A.ESTABS"""
+ ismatin
+ modul
+ """
AND A.JOUR BETWEEN %(debut)s AND %(fin)s
AND A.JOUR BETWEEN %(debut)s AND %(fin)s
""",
{
"etudid": etudid,
@ -639,7 +638,10 @@ def add_absence(
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
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(),
)
logdb(

View File

@ -218,7 +218,10 @@ def user_nbdays_since_last_notif(email_addr, etudid):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
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},
)
res = cursor.dictfetchone()

View File

@ -628,14 +628,18 @@ def AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id=None):
# supr les absences non justifiees
for date in dates:
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(),
)
sco_abs.invalidate_abs_etud_date(etudid, date)
# s'assure que les justificatifs ne sont pas "absents"
for date in dates:
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(),
)
if dates:
@ -840,9 +844,9 @@ def ListeAbsEtud(
# Formats non HTML et demande d'une seule table:
if format != "html" and format != "text":
if absjust_only == 1:
return tab_absjust.make_page(format=format, REQUEST=REQUEST)
return tab_absjust.make_page(format=format)
else:
return tab_absnonjust.make_page(format=format, REQUEST=REQUEST)
return tab_absnonjust.make_page(format=format)
if format == "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
<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
<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
<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.
"""
import os
import time
import datetime
import glob
import mimetypes
import os
import re
import shutil
import glob
import time
import flask
from flask import g
@ -244,31 +245,15 @@ class BaseArchiver(object):
log("reading archive file %s" % fname)
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"""
# XXX très incomplet: devrait inférer et assigner un type MIME
archive_id = self.get_id_from_name(oid, archive_name)
data = self.get(archive_id, filename)
ext = os.path.splitext(filename.lower())[1]
if ext == ".html" or ext == ".htm":
return data
elif ext == ".xml":
REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE)
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
mime = mimetypes.guess_type(filename)[0]
if mime is None:
mime = "application/octet-stream"
return scu.send_file(data, filename, mime=mime)
class SemsArchiver(BaseArchiver):
@ -305,7 +290,7 @@ def do_formsemestre_archive(
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
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)
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):
"""Page listing archives"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
sem_archive_id = formsemestre_id
L = []
for archive_id in PVArchive.list_obj_archives(sem_archive_id):
a = {
@ -559,11 +544,11 @@ def formsemestre_list_archives(REQUEST, formsemestre_id):
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."""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
sem_archive_id = sem["scodoc7_id"] or formsemestre_id
return PVArchive.get_archived_file(REQUEST, sem_archive_id, archive_name, filename)
sem_archive_id = formsemestre_id
return PVArchive.get_archived_file(sem_archive_id, archive_name, filename)
def formsemestre_delete_archive(
@ -575,7 +560,7 @@ def formsemestre_delete_archive(
"opération non autorisée pour %s" % str(REQUEST.AUTHENTICATED_USER)
)
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)
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:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etudid
etud_archive_id = etudid
L = []
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id):
a = {
@ -118,7 +118,7 @@ def add_archives_info_to_etud_list(etuds):
"""
for etud in etuds:
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):
l.append(
"%s (%s)"
@ -181,7 +181,7 @@ def etud_upload_file_form(REQUEST, etudid):
data = tf[2]["datafile"].read()
descr = tf[2]["description"]
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(
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:
raise ScoValueError("étudiant inexistant")
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)
if not dialog_confirmed:
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."""
etuds = sco_etud.get_etud_info(filled=True)
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["scodoc7_id"] or etud["etudid"]
return EtudsArchive.get_archived_file(
REQUEST, etud_archive_id, archive_name, filename
)
etud_archive_id = etud["etudid"]
return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename)
# --- 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.base import MIMEBase
from email.header import Header
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 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":
mod["mod_moy_txt"] = "-"
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_coef_txt"] = "Malus"
elif mod_moy < 0:
@ -1061,7 +1063,7 @@ def _formsemestre_bulletinetud_header_html(
# Menu
endpoint = "notes.formsemestre_bulletinetud"
url = REQUEST.URL0
qurl = six.moves.urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING)
qurl = urllib.parse.quote_plus(url + "?" + REQUEST.QUERY_STRING)
menuBul = [
{

View File

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

View File

@ -197,4 +197,4 @@ def formsemestre_estim_cost(
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,
javascripts=["js/etud_info.js"],
format=format,
REQUEST=REQUEST,
with_html_headers=True,
)

View File

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

View File

@ -578,7 +578,7 @@ def _view_etuds_page(
preferences=sco_preferences.SemPreferences(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
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"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
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"])
(
@ -753,7 +754,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html", REQUEST=None):
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
H += [
tab.html(),

View File

@ -31,27 +31,21 @@
# Ancien module "scolars"
import os
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 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
from app.scodoc.sco_utils import SCO_ENCODING
import app.scodoc.notesdb as ndb
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 sco_preferences
from app.scodoc.scolog import logdb
from flask_mail import Message
from app import mail
from app.scodoc.TrivialFormulator import TrivialFormulator
MONTH_NAMES_ABBREV = [
"Jan ",
@ -256,7 +250,6 @@ _identiteEditor = ndb.EditableTable(
"photo_filename",
"code_ine",
"code_nip",
"scodoc7_id",
),
filter_dept=True,
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)
if scu.FORBIDDEN_CHARS_EXP.search(nom) or scu.FORBIDDEN_CHARS_EXP.search(prenom):
return False, 0
# Now count homonyms:
# Now count homonyms (dans tous les départements):
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
req = """SELECT id
FROM identite
@ -440,7 +433,7 @@ def notify_etud_change(email_addr, etud, before, after, subject):
"Civilité: " + etud["civilite_str"],
"Nom: " + etud["nom"],
"Prénom: " + etud["prenom"],
"Etudid: " + etud["etudid"],
"Etudid: " + str(etud["etudid"]),
"\n",
"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: subject: %s" % subject)
log(txt)
mail.send_email(
email.send_email(
subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt
)
return txt
@ -896,7 +889,7 @@ def list_scolog(etudid):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
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},
)
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é
"""
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:
# toto()
if style is None:

View File

@ -96,3 +96,20 @@ class ScoGenError(ScoException):
class ScoInvalidDateError(ScoValueError):
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]),
)
if format != "html":
return tab.make_page(
format=format, with_html_headers=False, REQUEST=REQUEST
)
return tab.make_page(format=format, with_html_headers=False)
tab_html = tab.html()
nb_rows = tab.get_nb_rows()
else:

View File

@ -225,7 +225,7 @@ def search_etuds_infos(expnom=None, code_nip=None):
else:
code_nip = code_nip or expnom
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:
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)
return tab.make_page(
format=format, with_html_headers=False, REQUEST=REQUEST, publish=True
)
return tab.make_page(format=format, with_html_headers=False, publish=True)

View File

@ -27,6 +27,7 @@
"""Operations de base sur les formsemestres
"""
from app.scodoc.sco_exceptions import ScoValueError
import time
from operator import itemgetter
@ -60,6 +61,7 @@ _formsemestreEditor = ndb.EditableTable(
"gestion_semestrielle",
"etat",
"bul_hide_xml",
"block_moyennes",
"bul_bgcolor",
"modalite",
"resp_can_edit",
@ -67,7 +69,6 @@ _formsemestreEditor = ndb.EditableTable(
"ens_can_edit_eval",
"elt_sem_apo",
"elt_annee_apo",
"scodoc7_id",
),
filter_dept=True,
sortkey="date_debut",
@ -81,6 +82,7 @@ _formsemestreEditor = ndb.EditableTable(
"etat": bool,
"gestion_compensation": bool,
"bul_hide_xml": bool,
"block_moyennes": bool,
"gestion_semestrielle": bool,
"gestion_compensation": bool,
"gestion_semestrielle": bool,
@ -93,6 +95,10 @@ _formsemestreEditor = ndb.EditableTable(
def get_formsemestre(formsemestre_id):
"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:
sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0]
return sem
@ -578,7 +584,7 @@ def view_formsemestre_by_etape(etape_apo=None, format="html", REQUEST=None):
</form>""",
)
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):

View File

@ -501,6 +501,14 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
"labels": [""],
},
),
(
"block_moyennes",
{
"input_type": "boolcheckbox",
"title": "Bloquer moyennes",
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
},
),
(
"sep",
{
@ -693,7 +701,6 @@ def do_formsemestre_createwithmodules(REQUEST=None, edit=False):
tf[2]["bul_hide_xml"] = False
else:
tf[2]["bul_hide_xml"] = True
# remap les identifiants de responsables:
tf[2]["responsable_id"] = User.get_user_id_from_nomplogin(
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):
"Définition du menu 'Statistiques' "
"Définition du menu 'Statistiques'"
return [
{
"title": "Statistiques...",
@ -495,6 +495,7 @@ def formsemestre_page_title():
if not formsemestre_id:
return ""
try:
formsemestre_id = int(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy()
except:
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 += ">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
@ -913,10 +914,10 @@ def formsemestre_status_head(formsemestre_id=None, REQUEST=None, page_title=None
html_sco_header.html_sem_header(
REQUEST, page_title, sem, with_page_header=False, with_h2=False
),
"""<table>
f"""<table>
<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>"""
% F,
<a href="{url_for('notes.ue_list', scodoc_dept=g.scodoc_dept, formation_id=F['formation_id'])}"
class="discretelink" title="Formation {F['acronyme']}, v{F['version']}">{F['titre']}</a>""",
]
if sem["semestre_id"] >= 0:
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>"""
)
H.append("</table>")
sem_warning = ""
if sem["bul_hide_xml"]:
H.append(
'<p class="fontorange"><em>Bulletins non publiés sur le portail</em></p>'
)
sem_warning += "Bulletins non publiés sur le portail. "
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):
H.append(
'<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
R = [g for g in groups if g["partition_name"] is 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

View File

@ -477,6 +477,9 @@ def groups_table(
[(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)
columns_ids.append("etat")
@ -500,11 +503,7 @@ def groups_table(
if with_annotations:
sco_etud.add_annotations_to_etud_list(groups_infos.members)
columns_ids += ["annotations_str"]
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_sem_name = groups_infos.formsemestre["session_id"]
moodle_groupenames = set()
# ajoute liens
for etud in groups_infos.members:
@ -529,23 +528,27 @@ def groups_table(
# et groupes:
for partition_id in etud["partitions"]:
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 = []
if groups_infos.selected_partitions:
# il y a des groupes selectionnes, utilise leurs partitions
for partition_id in groups_infos.selected_partitions:
if partition_id in etud["partitions"]:
moodle_groupename.append(
etud["partitions"][partition_id]["group_name"]
partitions_name[partition_id]
+ "-"
+ etud["partitions"][partition_id]["group_name"]
)
else:
# pas de groupes sélectionnés: prend le premier s'il y en a un
moodle_groupename = ["tous"]
if etud["partitions"]:
for p in etud["partitions"].items(): # partitions is an OrderedDict
moodle_groupename = [
partitions_name[p[0]] + "-" + p[1]["group_name"]
]
break
moodle_groupename = [p[1]["group_name"]]
else:
moodle_groupename = ["tous"]
moodle_groupenames |= set(moodle_groupename)
etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename)
@ -706,7 +709,7 @@ def groups_table(
):
if format == "moodlecsv":
format = "csv"
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
elif format == "xlsappel":
xls = sco_excel.excel_feuille_listeappel(
@ -935,7 +938,7 @@ def form_choix_saisie_semaine(groups_infos, REQUEST=None):
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
Each (student,group) will be listed on a separate line
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"],
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):
"""Liste les etudiants inscrits dans n'importe quel semestre
du même département
SAUF sem à la date de début de sem.
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
cursor.execute(
"""SELECT I.etudid
FROM notes_formsemestre_inscription I, notes_formsemestre S
WHERE I.formsemestre_id = S.id
"""SELECT ins.etudid
FROM
notes_formsemestre_inscription ins,
notes_formsemestre S
WHERE ins.formsemestre_id = S.id
AND S.id != %(formsemestre_id)s
AND S.date_debut <= %(date_debut_iso)s
AND S.date_fin >= %(date_debut_iso)s
AND S.dept_id = %(dept_id)s
""",
sem,
)

View File

@ -27,8 +27,8 @@
"""Liste des notes d'une évaluation
"""
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
from operator import itemgetter
import urllib
import flask
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_preferences
from app.scodoc import sco_etud
from app.scodoc import sco_users
import sco_version
from app.scodoc.gen_tables import GenTable
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
)
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":
return t
@ -569,7 +570,7 @@ def _add_eval_columns(
comment = ""
explanation = "%s (%s) %s" % (
NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
NotesDB[etudid]["uid"],
sco_users.user_info(NotesDB[etudid]["uid"])["nomcomplet"],
comment,
)
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>'
% (
etud["etudid"],
six.moves.urllib.parse.quote(E["jour"]),
six.moves.urllib.parse.quote(E["jour"]),
urllib.parse.quote(E["jour"]),
urllib.parse.quote(E["jour"]),
demijournee,
E["moduleimpl_id"],
)

View File

@ -85,7 +85,7 @@ def scodoc_table_etuds_lycees(format="html", REQUEST=None):
no_links=True,
)
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":
return t
H = [
@ -192,7 +192,7 @@ def formsemestre_etuds_lycees(
tab.base_url += "&only_primo=1"
if no_grouping:
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":
return t
F = [

View File

@ -529,10 +529,10 @@ def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id, REQUEST=None):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""DELETE FROM notes_moduleimpl_inscription
"""DELETE FROM notes_moduleimpl_inscription
WHERE moduleimpl_inscription_id IN (
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
WHERE sem.formsemestre_id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.formsemestre_id

View File

@ -28,7 +28,7 @@
"""Tableau de bord module
"""
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_login import current_user
@ -137,7 +137,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0, REQUEST=None):
"title": "Absences ce jour",
"endpoint": "absences.EtatAbsencesDate",
"args": {
"date": six.moves.urllib.parse.quote(E["jour"], safe=""),
"date": urllib.parse.quote(E["jour"], safe=""),
"group_ids": group_id,
},
"enabled": E["jour"],

View File

@ -45,13 +45,20 @@ class Permission(object):
NBITS = 1 # maximum bits used (for formatting)
ALL_PERMISSIONS = [-1]
description = {} # { symbol : blah blah }
permission_by_name = {} # { symbol : int }
@staticmethod
def init_permissions():
for (perm, symbol, description) in _SCO_PERMISSIONS:
setattr(Permission, symbol, perm)
Permission.description[symbol] = description
Permission.permission_by_name[symbol] = perm
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()

View File

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

View File

@ -112,6 +112,7 @@ get_base_preferences(formsemestre_id)
"""
import flask
from flask import g, url_for
from flask_login import current_user
from app.models import Departement
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):
"""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
# search in scu.CONFIG
@ -1408,7 +1409,7 @@ class BasePreferences(object):
{
"initvalue": 1,
"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",
"labels": ["non", "oui"],
"category": "bul",
@ -1891,7 +1892,7 @@ class BasePreferences(object):
def get(self, formsemestre_id, name):
"""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 = {
"dept_id": self.dept_id,
@ -1901,7 +1902,7 @@ class BasePreferences(object):
cnx = ndb.GetDBConnexion()
plist = self._editor.list(cnx, params)
if not plist:
del params["formsemestre_id"]
params["formsemestre_id"] = None
plist = self._editor.list(cnx, params)
if not plist:
return self.default[name]
@ -2022,7 +2023,9 @@ class BasePreferences(object):
html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
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="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():
""" Liste les preferences en MarkDown, pour la documentation"""
"""Liste les preferences en MarkDown, pour la documentation"""
L = []
for cat, cat_descr in PREF_CATEGORIES:
L.append([""])

View File

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

View File

@ -27,8 +27,9 @@
"""Tableau recapitulatif des notes d'un semestre
"""
import time
import datetime
import json
import time
from xml.etree import ElementTree
import app.scodoc.sco_utils as scu
@ -227,11 +228,14 @@ def do_formsemestre_recapcomplet(
if format == "xml" or format == "html":
return data
elif format == "csv":
return scu.sendCSVFile(REQUEST, data, filename)
elif format[:3] == "xls":
return sco_excel.send_excel_file(REQUEST, data, filename)
return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
elif format[:3] == "xls" or format[:3] == "xlsx":
return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE)
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:
raise ValueError("unknown format %s" % format)

View File

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

View File

@ -9,48 +9,50 @@ from app.scodoc.sco_permissions import Permission as p
SCO_ROLES_DEFAULTS = {
"Observateur": (p.ScoObservateur,),
"Ens": (
p.ScoObservateur,
p.ScoView,
p.ScoEnsView,
p.ScoUsersView,
p.ScoEtudAddAnnotations,
p.ScoAbsChange,
p.ScoAbsAddBillet,
p.ScoAbsChange,
p.ScoEnsView,
p.ScoEntrepriseView,
p.ScoEtudAddAnnotations,
p.ScoObservateur,
p.ScoUsersView,
p.ScoView,
),
"Secr": (
p.ScoObservateur,
p.ScoView,
p.ScoUsersView,
p.ScoEtudAddAnnotations,
p.ScoAbsChange,
p.ScoAbsAddBillet,
p.ScoEntrepriseView,
p.ScoAbsChange,
p.ScoEditApo,
p.ScoEntrepriseChange,
p.ScoEntrepriseView,
p.ScoEtudAddAnnotations,
p.ScoEtudChangeAdr,
p.ScoObservateur,
p.ScoUsersView,
p.ScoView,
),
# Admin est le chef du département, pas le "super admin"
# on doit donc lister toutes ses permissions:
"Admin": (
p.ScoObservateur,
p.ScoView,
p.ScoEnsView,
p.ScoUsersView,
p.ScoEtudAddAnnotations,
p.ScoAbsChange,
p.ScoAbsAddBillet,
p.ScoEntrepriseView,
p.ScoEntrepriseChange,
p.ScoEtudChangeAdr,
p.ScoAbsChange,
p.ScoChangeFormation,
p.ScoEditFormationTags,
p.ScoEditAllNotes,
p.ScoChangePreferences,
p.ScoEditAllEvals,
p.ScoImplement,
p.ScoEditAllNotes,
p.ScoEditApo,
p.ScoEditFormationTags,
p.ScoEnsView,
p.ScoEntrepriseChange,
p.ScoEntrepriseView,
p.ScoEtudAddAnnotations,
p.ScoEtudChangeAdr,
p.ScoEtudChangeGroups,
p.ScoEtudInscrit,
p.ScoImplement,
p.ScoObservateur,
p.ScoUsersAdmin,
p.ScoChangePreferences,
p.ScoUsersView,
p.ScoView,
),
# RespPE est le responsable poursuites d'études
# 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)
cursor.execute(
"""INSERT INTO notes_notes
(etudid,evaluation_id,value,comment,date,uid)
VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)""",
"""INSERT INTO notes_notes
(etudid, evaluation_id, value, comment, date, uid)
VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)
""",
aa,
)
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
if do_it:
cursor.execute(
"""INSERT INTO notes_notes_log
"""INSERT INTO notes_notes_log
(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
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 do_it:
cursor.execute(
"""UPDATE notes_notes
SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
"""UPDATE notes_notes
SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
WHERE etudid = %(etudid)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)
)
cursor.execute(
"""DELETE FROM notes_notes
"""DELETE FROM notes_notes
WHERE etudid = %(etudid)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):
"""Saisie des notes via un fichier Excel"""
authuser = REQUEST.AUTHENTICATED_USER
authusername = str(authuser)
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
E = evals[0]
M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
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 (
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
avez l'autorisation d'effectuer cette opération)</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):
"""Formulaire saisie notes d'une évaluation pour un groupe"""
authuser = REQUEST.AUTHENTICATED_USER
authusername = str(authuser)
group_ids = [int(group_id) for group_id in group_ids]
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
@ -871,10 +869,11 @@ def saisie_notes(evaluation_id, group_ids=[], REQUEST=None):
formsemestre_id = M["formsemestre_id"]
# Check access
# (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 (
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
avez l'autorisation d'effectuer cette opération)</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": [
'class="note%s"' % classdem,
disabled_attr,
"data-last-saved-value=%s" % e["val"],
"data-orig-value=%s" % e["val"],
"data-etudid=%s" % etudid,
'data-last-saved-value="%s"' % e["val"],
'data-orig-value="%s"' % e["val"],
'data-etudid="%s"' % etudid,
],
"template": """<tr%(item_dom_attr)s class="etud_elem """
+ " ".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
else:
# ancienne valeur
nv = '<span class="histvalue">: %s</span>' % dispnote
nv = ": %s" % dispnote
first = False
if 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:
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(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
page_title = "Ensembles de semestres"
H = [

View File

@ -400,7 +400,10 @@ def list_synch(sem, anneeapogee=None):
def key2etud(key, etud_apo=False):
if not etud_apo:
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[
"datefinalisationinscription"
@ -508,7 +511,14 @@ def list_all(etudsapo_set):
# d'interrogation par etudiant.
cnx = ndb.GetDBConnexion()
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()])
all_set = set(key2etudid.keys())

View File

@ -167,7 +167,7 @@ def evaluation_list_operations(evaluation_id, REQUEST=None):
% (E["description"], E["jour"]),
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):
@ -222,7 +222,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html", REQUEST=None
+ 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=""):

View File

@ -217,7 +217,7 @@ def list_users(
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):
@ -258,6 +258,7 @@ def user_info(user_name_or_id=None, user=None):
info = u.to_dict()
else:
info = None
user_name = "inconnu"
else:
info = user.to_dict()
user_name = user.user_name

View File

@ -2612,7 +2612,8 @@ div.maindiv {
margin: 1em;
}
ul.main {
list-style-type: square;
list-style-type: square;
margin-top: 1em;
}
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 %}
{% block app_content %}
<h1>Sign In</h1>
<h1>Connexion</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
<br>
Forgot Your Password?
<a href="{{ url_for('auth.reset_password_request') }}">Click to Reset It</a>
En cas d'oubli de votre mot de passe
<a href="{{ url_for('auth.reset_password_request') }}">cliquez ici pour le réinitialiser</a>.
</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 %}

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
indiquant la version du logiciel
<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>).
</p>

View File

@ -24,9 +24,9 @@
<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
indiquant la version du logiciel (ScoDoc {SCOVERSION})
<br />(pour plus d'informations sur les listes de diffusion <a
href="https://scodoc.org/ListesDeDiffusion/">voir cette page</a>).
indiquant la version du logiciel (ScoDoc {{SCOVERSION}})
<br />(pour plus d'informations sur les listes de diffusion
<a href="https://scodoc.org/ListesDeDiffusion/">voir cette page</a>).
</p>
</body>

View File

@ -2,7 +2,7 @@
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h2>ScoDoc: gestion scolarité (version béta)</h2>
<h2>ScoDoc 9 - suivi scolarité</h2>
{% if not current_user.is_anonymous %}
<p>Bonjour <font color="red"><b>{{current_user.get_nomcomplet()}}</b>
@ -24,10 +24,6 @@
{% endfor %}
</ul>
<p>
<font color="red">Ceci est une version de test,
ne pas utiliser en production !</font>
</p>
{% if current_user.is_authenticated %}
<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>
</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 %}

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 cgi
import datetime
import dateutil
import dateutil.parser
import re
import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
import string
import time
import urllib
from xml.etree import ElementTree
import flask
from flask import g
from flask import url_for
from flask import current_app
from app.decorators import (
scodoc,
@ -79,7 +76,7 @@ from app.scodoc import notesdb as ndb
from app import log
from app.scodoc.scolog import logdb
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.gen_tables import GenTable
from app.scodoc import html_sco_header
@ -277,8 +274,9 @@ def doSignaleAbsenceGrSemestre(
Efface les absences aux dates indiquées par dates,
ou bien ajoute celles de abslist.
"""
moduleimpl_id = moduleimpl_id or None
if etudids:
etudids = etudids.split(",")
etudids = [int(x) for x in str(etudids).split(",")]
else:
etudids = []
if dates:
@ -306,14 +304,14 @@ def doSignaleAbsenceGrSemestre(
@permission_required(Permission.ScoAbsChange)
@scodoc7func
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"
if not moduleimpl_id:
moduleimpl_id = None
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:
return (
@ -325,7 +323,7 @@ def SignaleAbsenceGrHebdo(
base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % (
datelundi,
groups_infos.groups_query_args,
six.moves.urllib.parse.quote(destination),
urllib.parse.quote(destination),
)
formsemestre_id = groups_infos.formsemestre_id
@ -509,7 +507,7 @@ def SignaleAbsenceGrSemestre(
datedebut,
datefin,
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
@ -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="destination" value="%s"/>'
% six.moves.urllib.parse.quote(destination)
% urllib.parse.quote(destination)
)
#
# 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()),
)
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
@bp.route("/EtatAbsencesDate")
@ -1101,7 +1099,7 @@ def AddBilletAbsence(
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
tab = _tableBillets(billets, etud=etud)
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:
return billet_id
@ -1232,7 +1230,7 @@ def listeBilletsEtud(etudid=False, REQUEST=None, format="html"):
cnx = ndb.GetDBConnexion()
billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]})
tab = _tableBillets(billets, etud=etud)
return tab.make_page(REQUEST=REQUEST, format=format)
return tab.make_page(format=format)
@bp.route(
@ -1479,20 +1477,24 @@ def ProcessBilletAbsenceForm(billet_id, REQUEST=None):
def XMLgetAbsEtud(beg_date="", end_date="", REQUEST=None):
"""returns list of absences in date interval"""
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])$")
if not exp.match(beg_date):
raise ScoValueError("invalid date: %s" % beg_date)
if not exp.match(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)
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
doc.append(
ElementTree.Element(
@ -1500,7 +1502,7 @@ def XMLgetAbsEtud(beg_date="", end_date="", REQUEST=None):
begin=a["begin"],
end=a["end"],
description=a["description"],
justified=a["estjust"],
justified=str(int(a["estjust"])),
)
)
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(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
else:
H = [
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(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
H.append(tab.html())
@ -403,7 +403,7 @@ def entreprise_correspondant_list(
preferences=context.get_preferences(),
)
if format != "html":
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
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)
doc = ElementTree.Element("formsemestrelist")
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)
@ -1086,7 +1090,7 @@ def view_module_abs(REQUEST, moduleimpl_id, 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()
@ -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.",
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"])

View File

@ -37,6 +37,7 @@ import flask
from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request
from flask.app import Flask
import flask_login
from flask_login.utils import login_required
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
@ -57,6 +58,7 @@ from app.scodoc import sco_utils as scu
from app.decorators import (
admin_required,
scodoc7func,
scodoc,
permission_required_compat_scodoc7,
)
from app.scodoc.sco_permissions import Permission
@ -131,6 +133,43 @@ def get_etud_dept():
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

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
@ -307,19 +279,10 @@ def showEtudLog(etudid, format="html", REQUEST=None):
preferences=sco_preferences.SemPreferences(),
)
return tab.make_page(format=format, REQUEST=REQUEST)
return tab.make_page(format=format)
# ---------- 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("/")

View File

@ -97,11 +97,12 @@ def user_info(user_name, format="json", REQUEST=None):
@scodoc
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func
def create_user_form(REQUEST, user_name=None, edit=0):
"form. creation ou edit utilisateur"
def create_user_form(REQUEST, user_name=None, edit=0, all_roles=1):
"form. création ou edition utilisateur"
auth_dept = current_user.dept
initvalues = {}
edit = int(edit)
all_roles = int(all_roles)
H = [html_sco_header.sco_header(bodyOnLoad="init_tf_form('')")]
F = html_sco_header.sco_footer()
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>""")
is_super_admin = True
# Les rôles standards créés à l'initialisation de ScoDoc:
standard_roles = [
Role.get_named_role(r) for r in ("Ens", "Secr", "Admin", "RespPe")
]
# Rôles pouvant etre attribués aux utilisateurs via ce dialogue:
if all_roles:
# tous sauf SuperAdmin
standard_roles = [
r
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
# sinon, les départements dans lesquels l'utilisateur a le droit
if is_super_admin:
@ -209,7 +218,7 @@ def create_user_form(REQUEST, user_name=None, edit=0):
},
),
(
"passwd",
"password",
{
"title": "Mot de passe",
"input_type": "password",
@ -219,7 +228,7 @@ def create_user_form(REQUEST, user_name=None, edit=0):
},
),
(
"passwd2",
"password2",
{
"title": "Confirmer mot de passe",
"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
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:
del vals["dept"]
if "passwd" in vals:
del vals["passwd"]
if "password" in vals:
del vals["passwordd"]
if "date_modif_passwd" in vals:
del vals["date_modif_passwd"]
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
# check passwords
if vals["passwd"]:
if vals["passwd"] != vals["passwd2"]:
if vals["password"]:
if vals["password"] != vals["password2"]:
msg = tf_error_message(
"""Les deux mots de passes ne correspondent pas !"""
)
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(
"""Mot de passe trop simple, recommencez !"""
)

View File

@ -30,6 +30,8 @@ class Config:
SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc")
SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data")
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
@ -53,7 +55,7 @@ class DevConfig(Config):
DEBUG = True
TESTING = False
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"
@ -62,7 +64,7 @@ class TestConfig(DevConfig):
TESTING = True
DEBUG = True
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"
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)
1) Sur la machine origine, faire un dump complet:
su postgres
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:
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:
su postgres
su scodoc
psql -l
liste les bases: celles de ScoDoc sont SCO*
Pour chaque base SCO*, faire dropdb
dropdb SCOUSERS
dropdb SCOGEII
dropdb SCODOC
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
echo dropping $f
dropdb $f
done
1.2) Charger le dump (toujours comme utilisateur postgres):
psql -f scodoc.dump.txt postgres
1.2) Charger le dump (toujours comme utilisateur scodoc):
psql -f scodoc.dump.txt scodoc
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
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
avant de redemarrer scodoc, afin qu'il change si besoin la base de donnees:
(en tant que root):
cd /opt/scodoc/instance/Products/ScoDoc/config
./upgrade.sh
apt-get update &&
----
Cas d'une seule base à copier: (eg un seul département, mais faire
attention aux utilisateurs definis dans la base SCOUSERS):
Cas d'une seule base à copier (base production ou dev. par exemple)
En tant qu'utilisateur "postgres":
Dump: (script avec commande de creation de la base)
pg_dump --create SCOINFO > /tmp/scoinfo.dump
En tant qu'utilisateur "scodoc":
Dump: permettant de la recharger en changeant son nom
pg_dump --format=custom --file=/tmp/SCODOC.dump SCODOC
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):
createdb -E UTF-8 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

View File

@ -1,14 +1,14 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.0.13"
SCOVERSION = "9.0.28"
SCONAME = "ScoDoc"
SCONEWS = """
<h4>Année 2021</h4>
<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>Évaluations de type "deuxième session"</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
import re
import sys
import click
import flask
from flask.cli import with_appcontext
from app import create_app, cli, db
from app import initialize_scodoc_database
from app import clear_scodoc_cache
from app import models
from app.auth.models import User, Role, UserRole
from app import models
from app.models import ScoPreference
from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar, absences
import tools
@ -45,6 +43,7 @@ def make_shell_context():
"User": User,
"Role": Role,
"UserRole": UserRole,
"Permission": Permission,
"notes": notes,
"scolar": scolar,
"ndb": ndb,
@ -142,13 +141,90 @@ def user_password(username, password=None): # user-password
return 1
u = User.query.filter_by(user_name=username).first()
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
u.set_password(password)
db.session.add(u)
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()
@ -191,7 +267,7 @@ def create_dept(dept): # create-dept
@app.cli.command()
@with_appcontext
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.
This script is typically run as unix user "scodoc".
The original SCOUSERS database is left unmodified.
@ -206,18 +282,40 @@ def import_scodoc7_users(): # import-scodoc7-users
@click.argument("dept")
@click.argument("dept_db_name")
@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"""
dept_db_uri = f"postgresql:///{dept_db_name}"
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()
@with_appcontext
def clear_cache(): # clear-cache
"""Clear ScoDoc cache
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()
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,
gestion_compensation=None,
bul_hide_xml=None,
block_moyennes=None,
gestion_semestrielle=None,
bul_bgcolor=None,
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_dept import import_scodoc7_dept
from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archive

View File

@ -1,29 +1,28 @@
#!/bin/bash
# usage: backup_db2 dbname
# usage: backup_db9 dbname
# Dump une base postgresql, et garde plusieurs dumps dans le passe
# (configurable dans le script backup_rotation.sh)
# 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
# deux sauvegardes dépend de leur anciennete)
#
#
# 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
# 1- supprimer la base existante si elle existe: dropdb SCOXXX
# Puis en tant qu'utilisateur scodoc: su scodoc
# 1- supprimer la base existante si elle existe: dropdb SCODOC
#
# 2- recreer la base, vide: createdb -E UTF-8 SCOXXX
# (nom de la base: SCOXXX ou XXX=departement)
#
# 3- pg_restore -d SCOXXX SCOXXX_pgdump
# /opt/scodoc/tools/create_database.sh SCODOC
# 3- pg_restore -d SCODOC SCODOC_pgdump
#
# 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
DUMPBASE="$DBNAME"-BACKUPS
@ -50,4 +49,4 @@ pg_dump --format=t "$DBNAME" -f $DUMPFILE
gzip $DUMPFILE
# 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
# 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
#

View File

@ -11,7 +11,7 @@
#
# A adapter a vos besoins. Utilisation a vos risques et perils.
#
# E. Viennet, 2002
# E. Viennet, 2002, 2021
# Installation:
# 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:
SUPERVISORMAIL=emmanuel.viennet@example.com
CALLER=`basename $0`
MACHINE=`hostname -s`
CALLER=$(basename $0)
MACHINE=$(hostname -s)
# -----------------------------------------------------
@ -40,7 +40,7 @@ MACHINE=`hostname -s`
# ----------------------------------
terminate()
{
dateTest=`date`
dateTest=$(date)
mail -s "Attention: Probleme sauvegarde ScoDoc" $SUPERVISORMAIL <<EOF
The execution of script $CALLER was not successful on $MACHINE.
@ -57,7 +57,7 @@ EOF
echo "Look at logfile $logfile"
echo
echo "$CALLER terminated, exiting now with rc=1."
dateTest=`date`
dateTest=$(date)
echo "End of script at: $dateTest"
echo ""
@ -74,16 +74,16 @@ EOF
# --------------------------------------
rsync_mirror_to_remote()
{
echo "--------------- mirroring " $MACHINE:$srcdir " to " $remotehost:$destdir >> $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
echo "--------------- mirroring $MACHINE:$srcdir to $remotehost:$destdir" >> $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
if [ $? -ne 0 ]
then
echo Error in rsync: code=$?
terminate
fi
echo "ending at" `date` >> $logfile 2>&1
echo "ending at $(date)" >> $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) }')
# 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 "Version détectée dans le source: $SCODOC_RELEASE"
@ -89,7 +91,8 @@ fi
# Puis on déplace les fichiers de config (nginx, systemd, ...)
# nginx:
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
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"

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"
# --- 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 ]
then
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
missingok
rotate 64

View File

@ -27,6 +27,7 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location /ScoDoc/static {
# handle static files directly, without forwarding to the application
@ -40,5 +41,8 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
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_notifications",
"itemsuivi", # etudid n'était pas une clé
"adresse", # etudid n'était pas une clé
"admissions", # idem
"scolar_events",
}:
# tables avec "fausses" clés
# (l'object référencé a pu disparaitre)

View File

@ -204,6 +204,9 @@ do
systemctl restart postgresql
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
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
echo "Ce script recharge les donnees de votre installation ScoDoc 7"
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 -n "Voulez-vous poursuivre cette operation ? (y/n) [n]"
read -r ans