diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index b8c4eafae..f46153133 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -147,6 +147,52 @@ def get_preference(name, formsemestre_id=None): return get_base_preferences().get(formsemestre_id, name) +def _convert_pref_type(p, pref_spec): + """p est une ligne de la bd + {'id': , 'dept_id': , 'name': '', 'value': '', 'formsemestre_id': } + converti la valeur chane en le type désiré spécifié par pref_spec + """ + if "type" in pref_spec: + typ = pref_spec["type"] + if typ == "float": + # special case for float values (where NULL means 0) + if p["value"]: + p["value"] = float(p["value"]) + else: + p["value"] = 0.0 + else: + func = eval(typ) + p["value"] = func(p["value"]) + if pref_spec.get("input_type", None) == "boolcheckbox": + # boolcheckbox: la valeur stockée en base est une chaine "0" ou "1" + # que l'on ressort en True|False + if p["value"]: + try: + p["value"] = bool(int(p["value"])) + except ValueError: + log( + f"""Warning: invalid value for boolean pref in db: '{p["value"]}'""" + ) + p["value"] = False + else: + p["value"] = False # NULL (backward compat) + + +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. + """ + # XXX va changer avec la nouvelle base + # search in scu.CONFIG + if hasattr(scu.CONFIG, name): + value = getattr(scu.CONFIG, name) + log("sco_preferences: found default value in config for %s=%s" % (name, value)) + else: + # uses hardcoded default + value = pref_spec["initvalue"] + return value + + PREF_CATEGORIES = ( # sur page "Paramètres" ("general", {}), @@ -469,21 +515,27 @@ class BasePreferences(object): "abs_notification_mail_tmpl", { "initvalue": """ - --- Ceci est un message de notification automatique issu de ScoDoc --- + --- Ceci est un message de notification automatique issu de ScoDoc --- + L'étudiant %(nomprenom)s L'étudiant %(nomprenom)s + L'étudiant %(nomprenom)s + inscrit en %(inscription)s) inscrit en %(inscription)s) + inscrit en %(inscription)s) + a cumulé %(nbabsjust)s absences justifiées a cumulé %(nbabsjust)s absences justifiées - et %(nbabsnonjust)s absences NON justifiées. + a cumulé %(nbabsjust)s absences justifiées + et %(nbabsnonjust)s absences NON justifiées. - Le compte a pu changer depuis cet envoi, voir la fiche sur %(url_ficheetud)s. + Le compte a pu changer depuis cet envoi, voir la fiche sur %(url_ficheetud)s. - Votre dévoué serveur ScoDoc. + Votre dévoué serveur ScoDoc. - PS: Au dela de %(abs_notify_abs_threshold)s, un email automatique est adressé toutes les %(abs_notify_abs_increment)s absences. Ces valeurs sont modifiables dans les préférences de ScoDoc. - """, + PS: Au dela de %(abs_notify_abs_threshold)s, un email automatique est adressé toutes les %(abs_notify_abs_increment)s absences. Ces valeurs sont modifiables dans les préférences de ScoDoc. + """, "title": """Message notification e-mail""", "explanation": """Balises remplacées, voir la documentation""", "input_type": "textarea", @@ -826,14 +878,18 @@ class BasePreferences(object): "PV_INTRO", { "initvalue": """- - Vu l'arrêté du 3 août 2005 relatif au diplôme universitaire de technologie et notamment son article 4 et 6; - + Vu l'arrêté du 3 août 2005 relatif au diplôme universitaire de technologie et notamment son article 4 et 6; + + - - - vu l'arrêté n° %(Decnum)s du Président de l'%(UnivName)s; - + - + vu l'arrêté n° %(Decnum)s du Président de l'%(UnivName)s; + + - - - vu la délibération de la commission %(Type)s en date du %(Date)s présidée par le Chef du département; - """, + - + vu la délibération de la commission %(Type)s en date du %(Date)s présidée par le Chef du département; + """, "title": """Paragraphe d'introduction sur le PV""", "explanation": """Balises remplacées: %(Univname)s = nom de l'université, %(DecNum)s = numéro de l'arrêté, %(Date)s = date de la commission, %(Type)s = type de commission (passage ou délivrance), %(VDICode)s = code diplôme""", "input_type": "textarea", @@ -940,8 +996,8 @@ class BasePreferences(object): "PV_LETTER_PASSAGE_SIGNATURE", { "initvalue": """Pour le Directeur de l'IUT
- et par délégation
- Le Chef du département""", + et par délégation
+ Le Chef du département""", "title": """Signature des lettres individuelles de passage d'un semestre à l'autre""", "explanation": """%(DirectorName)s et %(DirectorTitle)s remplacés""", "input_type": "textarea", @@ -965,43 +1021,45 @@ class BasePreferences(object): "PV_LETTER_TEMPLATE", { "initvalue": """  - %(INSTITUTION_CITY)s, le %(date_jury)s - + %(INSTITUTION_CITY)s, le %(date_jury)s + - - à %(nomprenom)s - - %(domicile)s - %(codepostaldomicile)s %(villedomicile)s + + à %(nomprenom)s + + %(domicile)s + %(codepostaldomicile)s %(villedomicile)s - - Jury de %(type_jury)s
%(titre_formation)s
-
+ + Jury de %(type_jury)s
%(titre_formation)s
+
- - Le jury de %(type_jury_abbrv)s du département %(DeptName)s + + Le jury de %(type_jury_abbrv)s du département %(DeptName)s + s'est réuni le %(date_jury)s. s'est réuni le %(date_jury)s. - - Les décisions vous concernant sont : - + s'est réuni le %(date_jury)s. + + Les décisions vous concernant sont : + - %(prev_decision_sem_txt)s - - Décision %(decision_orig)s : %(decision_sem_descr)s - + %(prev_decision_sem_txt)s + + Décision %(decision_orig)s : %(decision_sem_descr)s + - - %(decision_ue_txt)s - + + %(decision_ue_txt)s + - - %(observation_txt)s - + + %(observation_txt)s + - %(autorisations_txt)s + %(autorisations_txt)s - %(diplome_txt)s - """, + %(diplome_txt)s + """, "title": """Lettre individuelle""", "explanation": """Balises remplacées et balisage XML, voir la documentation""", "input_type": "textarea", @@ -1362,24 +1420,24 @@ class BasePreferences(object): "bul_pdf_title", { "initvalue": """ - %(UnivName)s - - - %(InstituteName)s - - - RELEVÉ DE NOTES - + %(UnivName)s +
+ + %(InstituteName)s + + + RELEVÉ DE NOTES + - - %(nomprenom)s %(demission)s - + + %(nomprenom)s %(demission)s + - - Formation: %(titre_num)s - - Année scolaire: %(anneescolaire)s - """, + + Formation: %(titre_num)s + + Année scolaire: %(anneescolaire)s + """, "title": "Bulletins PDF: paragraphe de titre", "explanation": "(balises interprétées, voir documentation)", "input_type": "textarea", @@ -1404,10 +1462,10 @@ class BasePreferences(object): "bul_pdf_sig_left", { "initvalue": """La direction des études -
- %(responsable)s -
- """, +
+ %(responsable)s +
+ """, "title": "Bulletins PDF: signature gauche", "explanation": "(balises interprétées, voir documentation)", "input_type": "textarea", @@ -1420,10 +1478,10 @@ class BasePreferences(object): "bul_pdf_sig_right", { "initvalue": """Le chef de département -
- %(ChiefDeptName)s -
- """, +
+ %(ChiefDeptName)s +
+ """, "title": "Bulletins PDF: signature droite", "explanation": "(balises interprétées, voir documentation)", "input_type": "textarea", @@ -1799,88 +1857,57 @@ class BasePreferences(object): def load(self): """Load all preferences from db""" log(f"loading preferences for dept_id={self.dept_id}") - try: - scu.GSL.acquire() - cnx = ndb.GetDBConnexion() - preflist = self._editor.list(cnx, {"dept_id": self.dept_id}) - self.prefs = {None: {}} # { formsemestre_id (or None) : { name : value } } - self.default = {} # { name : default_value } - for p in preflist: - if not p["formsemestre_id"] in self.prefs: - self.prefs[p["formsemestre_id"]] = {} - # Ignore les noms de préférences non utilisés dans le code: - if p["name"] not in self.prefs_dict: - continue - # Convert types: - if ( - p["name"] in self.prefs_dict - and "type" in self.prefs_dict[p["name"]] - ): - typ = self.prefs_dict[p["name"]]["type"] - if typ == "float": - # special case for float values (where NULL means 0) - if p["value"]: - p["value"] = float(p["value"]) - else: - p["value"] = 0.0 - else: - func = eval(typ) - p["value"] = func(p["value"]) - if ( - p["name"] in self.prefs_dict - and self.prefs_dict[p["name"]].get("input_type", None) - == "boolcheckbox" - ): - # boolcheckbox: la valeur stockée en base est une chaine "0" ou "1" - # que l'on ressort en True|False - if p["value"]: - try: - p["value"] = bool(int(p["value"])) - except ValueError: - log( - f"""Warning: invalid value for boolean pref in db: '{p["value"]}'""" - ) - p["value"] = False - else: - p["value"] = False # NULL (backward compat) - self.prefs[p["formsemestre_id"]][p["name"]] = p["value"] - # add defaults for missing prefs - for pref in self.prefs_definition: - name = pref[0] - # search preferences in configuration file - if name and name[0] != "_" and name not in self.prefs[None]: - # search in scu.CONFIG - if hasattr(scu.CONFIG, name): - value = getattr(scu.CONFIG, name) - log( - "sco_preferences: found default value in config for %s=%s" - % (name, value) - ) - else: - # uses hardcoded default - value = pref[1]["initvalue"] + cnx = ndb.GetDBConnexion() + preflist = self._editor.list(cnx, {"dept_id": self.dept_id}) + self.prefs = {None: {}} # { formsemestre_id (or None) : { name : value } } + self.default = {} # { name : default_value } + for p in preflist: + if not p["formsemestre_id"] in self.prefs: + self.prefs[p["formsemestre_id"]] = {} + # Ignore les noms de préférences non utilisés dans le code: + if p["name"] not in self.prefs_dict: + continue - self.default[name] = value - self.prefs[None][name] = value - log("creating missing preference for %s=%s" % (name, value)) - # add to db table - self._editor.create( - cnx, {"dept_id": self.dept_id, "name": name, "value": value} - ) - finally: - scu.GSL.release() + # Convert types: + if p["name"] in self.prefs_dict: + _convert_pref_type(p, self.prefs_dict[p["name"]]) + + self.prefs[p["formsemestre_id"]][p["name"]] = p["value"] + + # add defaults for missing prefs + for pref in self.prefs_definition: + name = pref[0] + # search preferences in configuration file + if name and name[0] != "_" and name not in self.prefs[None]: + value = _get_pref_default_value_from_config(name, pref[1]) + self.default[name] = value + self.prefs[None][name] = value + log("creating missing preference for %s=%s" % (name, value)) + # add to db table + self._editor.create( + cnx, {"dept_id": self.dept_id, "name": name, "value": value} + ) def get(self, formsemestre_id, name): """Returns preference value. - If no value defined for this semestre, returns global value. + If global_lookup, when no value defined for this semestre, returns global value. """ - if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]: - return self.prefs[formsemestre_id][name] - elif name in self.prefs[None]: - return self.prefs[None][name] - else: - return self.default[name] + params = { + "dept_id": self.dept_id, + "name": name, + "formsemestre_id": formsemestre_id, + } + cnx = ndb.GetDBConnexion() + plist = self._editor.list(cnx, params) + if not plist: + del params["formsemestre_id"] + plist = self._editor.list(cnx, params) + if not plist: + return self.default[name] + p = plist[0] + _convert_pref_type(p, self.prefs_dict[name]) + return p["value"] def __contains__(self, item): return item in self.prefs[None] @@ -1890,74 +1917,75 @@ class BasePreferences(object): def is_global(self, formsemestre_id, name): "True if name if not defined for semestre" - if ( - not (formsemestre_id in self.prefs) - or not name in self.prefs[formsemestre_id] - ): - return True - else: - return False + params = { + "dept_id": self.dept_id, + "name": name, + "formsemestre_id": formsemestre_id, + } + cnx = ndb.GetDBConnexion() + plist = self._editor.list(cnx, params) + return len(plist) == 0 def save(self, formsemestre_id=None, name=None): """Write one or all (if name is None) values to db""" - try: - scu.GSL.acquire() - modif = False - cnx = ndb.GetDBConnexion() - if name is None: - names = list(self.prefs[formsemestre_id].keys()) - else: - names = [name] - for name in names: - value = self.get(formsemestre_id, name) - if self.prefs_dict[name].get("input_type", None) == "boolcheckbox": - # repasse les booleens en chaines "0":"1" - value = "1" if value else "0" - # existe deja ? - pdb = self._editor.list( + modif = False + cnx = ndb.GetDBConnexion() + if name is None: + names = list(self.prefs[formsemestre_id].keys()) + else: + names = [name] + for name in names: + value = self.prefs[formsemestre_id][name] + if self.prefs_dict[name].get("input_type", None) == "boolcheckbox": + # repasse les booleens en chaines "0":"1" + value = "1" if value else "0" + # existe deja ? + pdb = self._editor.list( + cnx, + args={ + "dept_id": self.dept_id, + "formsemestre_id": formsemestre_id, + "name": name, + }, + ) + if not pdb: + # crée préférence + log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) + self._editor.create( cnx, - args={ + { "dept_id": self.dept_id, - "formsemestre_id": formsemestre_id, "name": name, + "value": value, + "formsemestre_id": formsemestre_id, }, ) - if not pdb: - # crée préférence - log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) - self._editor.create( + modif = True + log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) + else: + # edit existing value + + existing_value = pdb[0]["value"] # old stored value + if ( + (existing_value != value) + and (existing_value != str(value)) + and (existing_value or str(value)) + ): + self._editor.edit( cnx, { - "dept_id": self.dept_id, + "pref_id": pdb[0]["pref_id"], + "formsemestre_id": formsemestre_id, "name": name, "value": value, - "formsemestre_id": formsemestre_id, }, ) modif = True - log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) - else: - # edit existing value - if pdb[0]["value"] != str(value) and ( - pdb[0]["value"] or str(value) - ): - self._editor.edit( - cnx, - { - "pref_id": pdb[0]["pref_id"], - "formsemestre_id": formsemestre_id, - "name": name, - "value": value, - }, - ) - modif = True - log("save pref sem=%s %s=%s" % (formsemestre_id, name, value)) + log("save pref sem=%s %s=%s" % (formsemestre_id, name, value)) - # les preferences peuvent affecter les PDF cachés et les notes calculées: - if modif: - sco_cache.invalidate_formsemestre() - finally: - scu.GSL.release() + # les preferences peuvent affecter les PDF cachés et les notes calculées: + if modif: + sco_cache.invalidate_formsemestre() def set(self, formsemestre_id, name, value): if not name or name[0] == "_" or name not in self.prefs_name: @@ -1972,26 +2000,24 @@ class BasePreferences(object): def delete(self, formsemestre_id, name): if not formsemestre_id: raise ScoException() - try: - scu.GSL.acquire() - if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]: - del self.prefs[formsemestre_id][name] - cnx = ndb.GetDBConnexion() - pdb = self._editor.list( - cnx, args={"formsemestre_id": formsemestre_id, "name": name} - ) - if pdb: - log("deleting pref sem=%s %s" % (formsemestre_id, name)) - assert pdb[0]["dept_id"] == self.dept_id - self._editor.delete(cnx, pdb[0]["pref_id"]) - sco_cache.invalidate_formsemestre() # > modif preferences - finally: - scu.GSL.release() + + if formsemestre_id in self.prefs and name in self.prefs[formsemestre_id]: + del self.prefs[formsemestre_id][name] + cnx = ndb.GetDBConnexion() + pdb = self._editor.list( + cnx, args={"formsemestre_id": formsemestre_id, "name": name} + ) + if pdb: + log("deleting pref sem=%s %s" % (formsemestre_id, name)) + assert pdb[0]["dept_id"] == self.dept_id + self._editor.delete(cnx, pdb[0]["pref_id"]) + sco_cache.invalidate_formsemestre() # > modif preferences def edit(self, REQUEST): """HTML dialog: edit global preferences""" from app.scodoc import html_sco_header + self.load() H = [ html_sco_header.sco_header(page_title="Préférences"), "

Préférences globales pour %s

" % scu.ScoURL(), diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index cd5bddde3..c2df6738f 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -40,11 +40,11 @@ from app.scodoc.sco_exceptions import ScoValueError random.seed(12345) # tests reproductibles -DEMO_DIR = Config.SCODOC_DIR + "/tools/demo/" -NOMS = [x.strip() for x in open(DEMO_DIR + "/noms.txt").readlines()] -PRENOMS_H = [x.strip() for x in open(DEMO_DIR + "/prenoms-h.txt").readlines()] -PRENOMS_F = [x.strip() for x in open(DEMO_DIR + "/prenoms-f.txt").readlines()] -PRENOMS_X = [x.strip() for x in open(DEMO_DIR + "/prenoms-x.txt").readlines()] +NOMS_DIR = Config.SCODOC_DIR + "/tools/fakeportal/nomsprenoms" +NOMS = [x.strip() for x in open(NOMS_DIR + "/noms.txt").readlines()] +PRENOMS_H = [x.strip() for x in open(NOMS_DIR + "/prenoms-h.txt").readlines()] +PRENOMS_F = [x.strip() for x in open(NOMS_DIR + "/prenoms-f.txt").readlines()] +PRENOMS_X = [x.strip() for x in open(NOMS_DIR + "/prenoms-x.txt").readlines()] def id_generator(size=6, chars=string.ascii_uppercase + string.digits): diff --git a/tests/unit/test_departements.py b/tests/unit/test_departements.py index 83a6650b5..e8ca47f60 100644 --- a/tests/unit/test_departements.py +++ b/tests/unit/test_departements.py @@ -73,6 +73,8 @@ def test_preferences(test_client): assert len(prefs2) == len(prefs) prefs2.set(None, "abs_notification_mail_tmpl", "toto") assert prefs2.get(None, "abs_notification_mail_tmpl") == "toto" + # Vérifie que les prefs sont bien sur un seul département: + app.set_sco_dept(current_dept.acronym) assert prefs.get(None, "abs_notification_mail_tmpl") != "toto" orm_val = ( ScoPreference.query.filter_by(dept_id=d.id, name="abs_notification_mail_tmpl") @@ -82,6 +84,7 @@ def test_preferences(test_client): assert orm_val == "toto" # --- Preferences d'un semestre # rejoue ce test pour avoir un semestre créé + app.set_sco_dept("D2") test_sco_basic.run_sco_basic() sem = sco_formsemestre.do_formsemestre_list()[0] formsemestre_id = sem["formsemestre_id"]