diff --git a/README.md b/README.md index a5998e16..86b5d3a3 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,28 @@ Installation: donc utiliser: - pip install -r requirements.txt + pip install -r requirements-2.7.txt pour régénerer ce fichier: - pip freeze > requirements.txt + pip freeze > requirements-2.7.txt + +## Setup python3.7 +Debian 10 est livré avec Python 3.7. + + apt-get install python3-dev + apt-get install python3-venv + python3 -m venv venv + +Puis installation de Flask: + + source venv/bin/activate + pip install flask + pip install wheel + +Installer les dépendances: + + pip install -r requirements-2.7.txt ## Bases de données ScoDoc8 utilise les bases de département de ScoDoc7, mais une nouvelle base diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 1931bad7..85da8e68 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -43,11 +43,10 @@ Par exemple, la clé '_css_row_class' spécifie le style CSS de la ligne. from __future__ import print_function import random from collections import OrderedDict - -# XML generation package (apt-get install jaxml) -import jaxml +from xml.etree import ElementTree import json +from xml.etree import ElementTree from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from reportlab.lib.colors import Color @@ -59,6 +58,7 @@ from app.scodoc import html_sco_header from app.scodoc import sco_utils as scu from app.scodoc import sco_excel from app.scodoc import sco_pdf +from app.scodoc import sco_xml from app.scodoc.sco_pdf import SU from app.scodoc.notes_log import log @@ -567,28 +567,25 @@ class GenTable(object): The tag names and can be changed using xml_outer_tag and xml_row_tag """ - doc = jaxml.XML_document(encoding=scu.SCO_ENCODING) - getattr(doc, self.xml_outer_tag)( - id=self.table_id, origin=self.origin or "", caption=self.caption or "" + doc = ElementTree.Element( + self.xml_outer_tag, + id=self.table_id, + origin=self.origin or "", + caption=self.caption or "", ) - doc._push() for row in self.rows: - doc._push() + x_row = ElementTree.Element(self.xml_row_tag) row_title = row.get("row_title", "") if row_title: - getattr(doc, self.xml_row_tag)(title=row_title) - else: - getattr(doc, self.xml_row_tag)() + x_row.set("title", row_title) + doc.append(x_row) for cid in self.columns_ids: - doc._push() v = row.get(cid, "") if v is None: v = "" - getattr(doc, cid)(value=str(v)) - doc._pop() - doc._pop() - doc._pop() - return repr(doc) + x_cell = ElementTree.Element(cid, value=str(v)) + x_row.append(x_cell) + return sco_xml.XML_HEADER + ElementTree.tostring(doc) def json(self): """JSON representation of the table.""" diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 974ce478..03af2d70 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -102,7 +102,7 @@ import app.scodoc.notesdb as ndb from app.scodoc.notes_log import log from app.scodoc.sco_exceptions import ScoValueError, FormatError from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_formsemestre import ApoEtapeVDI +from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_codes_parcours import code_semestre_validant from app.scodoc.sco_codes_parcours import ( ADC, diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index fcfd857f..8acae1ba 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -38,7 +38,7 @@ from app.scodoc import sco_users from app.scodoc.gen_tables import GenTable from app.scodoc.notes_log import log from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_vdi import ApoEtapeVDI import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -385,100 +385,6 @@ def _write_formsemestre_aux(context, sem, fieldname, valuename): cnx.commit() -# ------ Utilisé pour stocker le VDI avec le code étape (noms de fichiers maquettes et code semestres) -class ApoEtapeVDI(object): - _ETAPE_VDI_SEP = "!" - - def __init__(self, etape_vdi=None, etape="", vdi=""): - """Build from string representation, e.g. 'V1RT!111'""" - if etape_vdi: - self.etape_vdi = etape_vdi - self.etape, self.vdi = self.split_etape_vdi(etape_vdi) - elif etape: - if self._ETAPE_VDI_SEP in etape: - raise ScoValueError("valeur code etape invalide") - self.etape, self.vdi = etape, vdi - self.etape_vdi = self.concat_etape_vdi(etape, vdi) - else: - self.etape_vdi, self.etape, self.vdi = "", "", "" - - def __repr__(self): - return self.__class__.__name__ + "('" + str(self) + "')" - - def __str__(self): - return self.etape_vdi - - def _cmp(self, other): - """Test égalité de deux codes étapes. - Si le VDI des deux est spécifié, on l'utilise. Sinon, seul le code étape est pris en compte. - Donc V1RT == V1RT!111, V1RT!110 == V1RT, V1RT!77 != V1RT!78, ... - - Compare the two objects x (=self) and y and return an integer according to - the outcome. The return value is negative if x < y, zero if x == y - and strictly positive if x > y. - """ - if other is None: - return -1 - if type(other) == str: - other = ApoEtapeVDI(other) - - if self.vdi and other.vdi: - x = (self.etape, self.vdi) - y = (other.etape, other.vdi) - else: - x = self.etape - y = other.etape - - return (x > y) - (x < y) - - def __eq__(self, other): - return self._cmp(other) == 0 - - def __ne__(self, other): - return self._cmp(other) != 0 - - def __lt__(self, other): - return self._cmp(other) < 0 - - def __le__(self, other): - return self._cmp(other) <= 0 - - def __gt__(self, other): - return self._cmp(other) > 0 - - def __ge__(self, other): - return self._cmp(other) >= 0 - - def split_etape_vdi(self, etape_vdi): - """Etape Apogee can be stored as 'V1RT' or, including the VDI version, - as 'V1RT!111' - Returns etape, VDI - """ - if etape_vdi: - t = etape_vdi.split(self._ETAPE_VDI_SEP) - if len(t) == 1: - etape = etape_vdi - vdi = "" - elif len(t) == 2: - etape, vdi = t - else: - raise ValueError("invalid code etape") - return etape, vdi - else: - return etape_vdi, "" - - def concat_etape_vdi(self, etape, vdi=""): - if vdi: - return self._ETAPE_VDI_SEP.join([etape, vdi]) - else: - return etape - - -""" -[ ApoEtapeVDI('V1RT!111'), ApoEtapeVDI('V1RT!112'), ApoEtapeVDI('VCRT'), ApoEtapeVDI('V1RT') ] -""" - - def sem_set_responsable_name(context, sem): "ajoute champs responsable_name" sem["responsable_name"] = ", ".join( diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 511ab7bb..3e8e6042 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -37,8 +37,8 @@ from app.scodoc import sco_groups from app.scodoc.notes_log import log from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.sco_exceptions import AccessDenied, ScoValueError -from app.scodoc.sco_formsemestre import ApoEtapeVDI from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours from app.scodoc import sco_compute_moy diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 56407b56..608d96cf 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -50,7 +50,7 @@ from app.scodoc.gen_tables import GenTable from app.scodoc.notes_log import log from app.scodoc.sco_etape_bilan import EtapeBilan from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc.sco_formsemestre import ApoEtapeVDI +from app.scodoc.sco_vdi import ApoEtapeVDI import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index bac16cdf..aec0db8f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -43,10 +43,10 @@ import time import types import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse -import xml.sax.saxutils + # XML generation package (apt-get install jaxml) -import jaxml +import jaxml # XXX try: import six @@ -66,8 +66,11 @@ from config import Config from app.scodoc.SuppressAccents import suppression_diacritics from app.scodoc.notes_log import log +from app.scodoc.sco_vdi import ApoEtapeVDI +from app.scodoc.sco_xml import quote_xml_attr from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL from app.scodoc import sco_exceptions +from app.scodoc import sco_xml from app.scodoc import VERSION # ----- TEMPORAIRE POUR MIGRATION SCODOC7 -> SCODOC8 avant python3 @@ -445,74 +448,6 @@ def unescape_html_dict(d): unescape_html_dict(v) -def quote_xml_attr(data): - """Escape &, <, >, quotes and double quotes""" - return xml.sax.saxutils.escape(str(data), {"'": "'", '"': """}) - - -def dict_quote_xml_attr(d, fromhtml=False): - """Quote XML entities in dict values. - Non recursive (but probbaly should be...). - Returns a new dict. - """ - if fromhtml: - # passe d'un code HTML a un code XML - return dict([(k, quote_xml_attr(unescape_html(v))) for (k, v) in d.items()]) - else: - # passe d'une chaine non quotée a du XML - return dict([(k, quote_xml_attr(v)) for (k, v) in d.items()]) - - -def simple_dictlist2xml(dictlist, doc=None, tagname=None, quote=False): - """Represent a dict as XML data. - All keys with string or numeric values are attributes (numbers converted to strings). - All list values converted to list of childs (recursively). - *** all other values are ignored ! *** - Values (xml entities) are not quoted, except if requested by quote argument. - - Exemple: - simple_dictlist2xml([ { 'id' : 1, 'ues' : [{'note':10},{}] } ], tagname='infos') - - - - - - - - """ - if not tagname: - raise ValueError("invalid empty tagname !") - if not doc: - doc = jaxml.XML_document(encoding=SCO_ENCODING) - scalar_types = [bytes, str, int, float] - for d in dictlist: - doc._push() - if ( - type(d) == types.InstanceType or type(d) in scalar_types - ): # pour ApoEtapeVDI et listes de chaines - getattr(doc, tagname)(code=str(d)) - else: - if quote: - d_scalar = dict( - [ - (k, quote_xml_attr(v)) - for (k, v) in d.items() - if type(v) in scalar_types - ] - ) - else: - d_scalar = dict( - [(k, v) for (k, v) in d.items() if type(v) in scalar_types] - ) - getattr(doc, tagname)(**d_scalar) - d_list = dict([(k, v) for (k, v) in d.items() if type(v) == list]) - if d_list: - for (k, v) in d_list.items(): - simple_dictlist2xml(v, doc=doc, tagname=k, quote=quote) - doc._pop() - return doc - - # Expressions used to check noms/prenoms FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]") ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE) @@ -615,15 +550,13 @@ def sendPDFFile(REQUEST, data, filename): class ScoDocJSONEncoder(json.JSONEncoder): def default(self, o): # pylint: disable=E0202 - from app.scodoc import sco_formsemestre - # ScoDoc 7.22 n'utilise plus mx: if str(type(o)) == "": log("Warning: mx.DateTime object detected !") return o.strftime("%Y-%m-%dT%H:%M:%S") if isinstance(o, (datetime.date, datetime.datetime)): return o.isoformat() - elif isinstance(o, sco_formsemestre.ApoEtapeVDI): + elif isinstance(o, ApoEtapeVDI): return str(o) else: return json.JSONEncoder.default(self, o) @@ -641,14 +574,9 @@ def sendXML(REQUEST, data, tagname=None, force_outer_xml_tag=True): data = [data] # always list-of-dicts if force_outer_xml_tag: root_tagname = tagname + "_list" - doc = jaxml.XML_document(encoding=SCO_ENCODING) - getattr(doc, root_tagname)() - doc._push() - else: - doc = None - doc = simple_dictlist2xml(data, doc=doc, tagname=tagname) - if force_outer_xml_tag: - doc._pop() + data = [{root_tagname: data}] + doc = sco_xml.simple_dictlist2xml(data, tagname=tagname) + if REQUEST: REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE) return repr(doc) diff --git a/app/scodoc/sco_vdi.py b/app/scodoc/sco_vdi.py new file mode 100644 index 00000000..fd7c35df --- /dev/null +++ b/app/scodoc/sco_vdi.py @@ -0,0 +1,121 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres) +""" +from app.scodoc.sco_exceptions import ScoValueError + + +class ApoEtapeVDI(object): + _ETAPE_VDI_SEP = "!" + + def __init__(self, etape_vdi=None, etape="", vdi=""): + """Build from string representation, e.g. 'V1RT!111'""" + if etape_vdi: + self.etape_vdi = etape_vdi + self.etape, self.vdi = self.split_etape_vdi(etape_vdi) + elif etape: + if self._ETAPE_VDI_SEP in etape: + raise ScoValueError("valeur code etape invalide") + self.etape, self.vdi = etape, vdi + self.etape_vdi = self.concat_etape_vdi(etape, vdi) + else: + self.etape_vdi, self.etape, self.vdi = "", "", "" + + def __repr__(self): + return self.__class__.__name__ + "('" + str(self) + "')" + + def __str__(self): + return self.etape_vdi + + def _cmp(self, other): + """Test égalité de deux codes étapes. + Si le VDI des deux est spécifié, on l'utilise. Sinon, seul le code étape est pris en compte. + Donc V1RT == V1RT!111, V1RT!110 == V1RT, V1RT!77 != V1RT!78, ... + + Compare the two objects x (=self) and y and return an integer according to + the outcome. The return value is negative if x < y, zero if x == y + and strictly positive if x > y. + """ + if other is None: + return -1 + if type(other) == str: + other = ApoEtapeVDI(other) + + if self.vdi and other.vdi: + x = (self.etape, self.vdi) + y = (other.etape, other.vdi) + else: + x = self.etape + y = other.etape + + return (x > y) - (x < y) + + def __eq__(self, other): + return self._cmp(other) == 0 + + def __ne__(self, other): + return self._cmp(other) != 0 + + def __lt__(self, other): + return self._cmp(other) < 0 + + def __le__(self, other): + return self._cmp(other) <= 0 + + def __gt__(self, other): + return self._cmp(other) > 0 + + def __ge__(self, other): + return self._cmp(other) >= 0 + + def split_etape_vdi(self, etape_vdi): + """Etape Apogee can be stored as 'V1RT' or, including the VDI version, + as 'V1RT!111' + Returns etape, VDI + """ + if etape_vdi: + t = etape_vdi.split(self._ETAPE_VDI_SEP) + if len(t) == 1: + etape = etape_vdi + vdi = "" + elif len(t) == 2: + etape, vdi = t + else: + raise ValueError("invalid code etape") + return etape, vdi + else: + return etape_vdi, "" + + def concat_etape_vdi(self, etape, vdi=""): + if vdi: + return self._ETAPE_VDI_SEP.join([etape, vdi]) + else: + return etape + + +# [ ApoEtapeVDI('V1RT!111'), ApoEtapeVDI('V1RT!112'), ApoEtapeVDI('VCRT'), ApoEtapeVDI('V1RT') ] diff --git a/app/scodoc/sco_xml.py b/app/scodoc/sco_xml.py new file mode 100644 index 00000000..403c468e --- /dev/null +++ b/app/scodoc/sco_xml.py @@ -0,0 +1,95 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + + +""" Exports XML +""" + +from xml.etree import ElementTree +import xml.sax.saxutils + +from app.scodoc.sco_vdi import ApoEtapeVDI + +XML_HEADER = """\n""" + + +def quote_xml_attr(data): + """Escape &, <, >, quotes and double quotes""" + return xml.sax.saxutils.escape(str(data), {"'": "'", '"': """}) + + +# ScoDoc7 legacy function: +def simple_dictlist2xml(dictlist, doc=None, tagname=None, quote=False): + """Represent a dict as XML data. + All keys with string or numeric values are attributes (numbers converted to strings). + All list values converted to list of childs (recursively). + *** all other values are ignored ! *** + Values (xml entities) are not quoted, except if requested by quote argument. + + Exemple: + simple_dictlist2xml([ { 'id' : 1, 'ues' : [{'note':10},{}] } ], tagname='infos') + + + + + + + + """ + if not tagname: + raise ValueError("invalid empty tagname !") + elements = _dictlist2xml(dictlist, root=[], tagname=tagname, quote=quote) + return XML_HEADER + "\n".join([ElementTree.tostring(x) for x in elements]) + + +def _dictlist2xml(dictlist, root=None, tagname=None, quote=False): + scalar_types = (bytes, str, int, float) + for d in dictlist: + elem = ElementTree.Element(tagname) + root.append(elem) + if isinstance(d, scalar_types) or isinstance(d, ApoEtapeVDI): + elem.set("code", str(d)) + else: + if quote: + d_scalar = dict( + [ + (k, quote_xml_attr(v)) + for (k, v) in d.items() + if isinstance(v, scalar_types) + ] + ) + else: + d_scalar = dict( + [(k, str(v)) for (k, v) in d.items() if isinstance(v, scalar_types)] + ) + for k in d_scalar: + elem.set(k, d_scalar[k]) + d_list = dict([(k, v) for (k, v) in d.items() if isinstance(v, list)]) + if d_list: + for (k, v) in d_list.items(): + _dictlist2xml(v, tagname=k, root=elem, quote=quote) + return root