# -*- 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
#
##############################################################################


""" Common definitions
"""
import os
import sys
import copy
import re
import bisect
import types
import numbers
import thread
import urllib2
import xml.sax.saxutils
import time
import datetime
import json

# XML generation package (apt-get install jaxml)
import jaxml

try:
    import six

    STRING_TYPES = six.string_types
except ImportError:
    # fallback for very old ScoDoc instances
    STRING_TYPES = types.StringType

from PIL import Image as PILImage

from VERSION import SCOVERSION
import VERSION

from SuppressAccents import suppression_diacritics
from notes_log import log
from sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL

# ----- CALCUL ET PRESENTATION DES NOTES
NOTES_PRECISION = 1e-4  # evite eventuelles erreurs d'arrondis
NOTES_MIN = 0.0  # valeur minimale admise pour une note (sauf malus, dans [-20, 20])
NOTES_MAX = 1000.0
NOTES_NEUTRALISE = -1000.0  # notes non prises en comptes dans moyennes
NOTES_SUPPRESS = -1001.0  # note a supprimer
NOTES_ATTENTE = -1002.0  # note "en attente" (se calcule comme une note neutralisee)


# Types de modules
MODULE_STANDARD = 0
MODULE_MALUS = 1

MALUS_MAX = 20.0
MALUS_MIN = -20.0

APO_MISSING_CODE_STR = "----"  # shown in HTML pages in place of missing code Apogée
EDIT_NB_ETAPES = 6  # Nombre max de codes étapes / semestre presentés dans l'UI

IT_SITUATION_MISSING_STR = (
    "____"  # shown on ficheEtud (devenir) in place of empty situation
)

RANG_ATTENTE_STR = "(attente)"  #  rang affiché sur bulletins quand notes en attente

# borne supérieure de chaque mention
NOTES_MENTIONS_TH = (
    NOTES_TOLERANCE,
    7.0,
    10.0,
    12.0,
    14.0,
    16.0,
    18.0,
    20.0 + NOTES_TOLERANCE,
)
NOTES_MENTIONS_LABS = (
    "Nul",
    "Faible",
    "Insuffisant",
    "Passable",
    "Assez bien",
    "Bien",
    "Très bien",
    "Excellent",
)

EVALUATION_NORMALE = 0
EVALUATION_RATTRAPAGE = 1


def fmt_note(val, note_max=None, keep_numeric=False):
    """conversion note en str pour affichage dans tables HTML ou PDF.
    Si keep_numeric, laisse les valeur numeriques telles quelles (pour export Excel)
    """
    if val is None:
        return "ABS"
    if val == NOTES_NEUTRALISE:
        return "EXC"  # excuse, note neutralise
    if val == NOTES_ATTENTE:
        return "ATT"  # attente, note neutralisee
    if type(val) == types.FloatType or type(val) == types.IntType:
        if note_max != None and note_max > 0:
            val = val * 20.0 / note_max
        if keep_numeric:
            return val
        else:
            s = "%2.2f" % round(float(val), 2)  # 2 chiffres apres la virgule
            s = "0" * (5 - len(s)) + s  # padding: 0 à gauche pour longueur 5: "12.34"
            return s
    else:
        return val.replace("NA0", "-")  # notes sans le NA0


def fmt_coef(val):
    """Conversion valeur coefficient (float) en chaine"""
    if val < 0.01:
        return "%g" % val  # unusually small value
    return "%g" % round(val, 2)


def fmt_abs(val):
    """Conversion absences en chaine. val est une list [nb_abs_total, nb_abs_justifiees
    => NbAbs / Nb_justifiees
    """
    return "%s / %s" % (val[0], val[1])


def isnumber(x):
    "True if x is a number (int, float, etc.)"
    return isinstance(x, numbers.Number)


def join_words(*words):
    words = [str(w).strip() for w in words if w is not None]
    return " ".join([w for w in words if w])


def get_mention(moy):
    """Texte "mention" en fonction de la moyenne générale"""
    try:
        moy = float(moy)
    except:
        return ""
    return NOTES_MENTIONS_LABS[bisect.bisect_right(NOTES_MENTIONS_TH, moy)]


# ----- Global lock for critical sections (except notes_tables caches)
GSL = thread.allocate_lock()  # Global ScoDoc Lock

if "INSTANCE_HOME" in os.environ:
    # ----- Repertoire "var" (local)
    SCODOC_VAR_DIR = os.path.join(os.environ["INSTANCE_HOME"], "var", "scodoc")
    # ----- Version information
    SCODOC_VERSION_DIR = os.path.join(SCODOC_VAR_DIR, "config", "version")
    # ----- Repertoire tmp
    SCO_TMPDIR = os.path.join(SCODOC_VAR_DIR, "tmp")
    if not os.path.exists(SCO_TMPDIR):
        os.mkdir(SCO_TMPDIR, 0o755)
    # ----- Les logos: /opt/scodoc/var/scodoc/config/logos
    SCODOC_LOGOS_DIR = os.path.join(SCODOC_VAR_DIR, "config", "logos")

    # ----- Repertoire "config" (devrait s'appeler "tools"...)
    SCO_CONFIG_DIR = os.path.join(
        os.environ["INSTANCE_HOME"], "Products", "ScoDoc", "config"
    )


# ----- Lecture du fichier de configuration
SCO_SRCDIR = os.path.split(VERSION.__file__)[0]
if SCO_SRCDIR:
    SCO_SRCDIR += "/"
else:
    SCO_SRCDIR = "/opt/scodoc/Products/ScoDoc/"  # debug mode
CONFIG = None
try:
    _config_filename = SCO_SRCDIR + "config/scodoc_config.py"
    _config_text = open(_config_filename).read()
except:
    sys.stderr.write("sco_utils: cannot open configuration file %s" % _config_filename)
    raise

try:
    exec(_config_text)
except:
    sys.stderr.write("sco_utils: error in configuration file %s" % _config_filename)
    raise

if hasattr(CONFIG, "CODES_EXPL"):
    CODES_EXPL.update(
        CONFIG.CODES_EXPL
    )  # permet de customiser les explications de codes


if CONFIG.CUSTOM_HTML_HEADER:
    CUSTOM_HTML_HEADER = open(CONFIG.CUSTOM_HTML_HEADER).read()
else:
    CUSTOM_HTML_HEADER = ""

if CONFIG.CUSTOM_HTML_HEADER_CNX:
    CUSTOM_HTML_HEADER_CNX = open(CONFIG.CUSTOM_HTML_HEADER_CNX).read()
else:
    CUSTOM_HTML_HEADER_CNX = ""

if CONFIG.CUSTOM_HTML_FOOTER:
    CUSTOM_HTML_FOOTER = open(CONFIG.CUSTOM_HTML_FOOTER).read()
else:
    CUSTOM_HTML_FOOTER = ""

if CONFIG.CUSTOM_HTML_FOOTER_CNX:
    CUSTOM_HTML_FOOTER_CNX = open(CONFIG.CUSTOM_HTML_FOOTER_CNX).read()
else:
    CUSTOM_HTML_FOOTER_CNX = ""

SCO_ENCODING = "utf-8"  # used by Excel, XML, PDF, ...
# Attention: encodage lié au codage Zope et aussi à celui de postgresql
#            et aussi a celui des fichiers sources Python (comme celui-ci).

# def to_utf8(s):
#    return unicode(s, SCO_ENCODING).encode('utf-8')


SCO_DEFAULT_SQL_USER = "www-data"  # should match Zope process UID
SCO_DEFAULT_SQL_PORT = (
    "5432"  # warning: 5433 for postgresql-8.1 on Debian if 7.4 also installed !
)
SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT

# Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés:
SCO_WEBSITE = "https://scodoc.org"
SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur"
SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces"
SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr"
SCO_USERS_LIST = "notes@listes.univ-paris13.fr"

# Mails avec exceptions (erreurs) anormales envoyés à cette adresse:
# mettre '' pour désactiver completement l'envois de mails d'erreurs.
# (ces mails sont précieux pour corriger les erreurs, ne les désactiver que si
#  vous avez de bonnes raisons de le faire: vous pouvez me contacter avant)
SCO_EXC_MAIL = "scodoc-exception@viennet.net"

# L'adresse du mainteneur (non utilisée automatiquement par ScoDoc: ne pas changer)
SCO_DEV_MAIL = "emmanuel.viennet@gmail.com"  # SVP ne pas changer

# Adresse pour l'envoi des dumps (pour assistance technnique):
#   ne pas changer (ou vous perdez le support)
SCO_DUMP_UP_URL = "https://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/upload-dump"

CSV_FIELDSEP = ";"
CSV_LINESEP = "\n"
CSV_MIMETYPE = "text/comma-separated-values"
XLS_MIMETYPE = "application/vnd.ms-excel"
PDF_MIMETYPE = "application/pdf"
XML_MIMETYPE = "text/xml"
JSON_MIMETYPE = "application/json"

LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png")  # remind that PIL does not read pdf


class DictDefault(dict):  # obsolete, use collections.defaultdict
    """A dictionnary with default value for all keys
    Each time a non existent key is requested, it is added to the dict.
    (used in python 2.4, can't use new __missing__ method)
    """

    defaultvalue = 0

    def __init__(self, defaultvalue=0, kv_dict={}):
        dict.__init__(self)
        self.defaultvalue = defaultvalue
        self.update(kv_dict)

    def __getitem__(self, k):
        if self.has_key(k):
            return self.get(k)
        value = copy.copy(self.defaultvalue)
        self[k] = value
        return value


class WrapDict:
    """Wrap a dict so that getitem returns '' when values are None"""

    def __init__(self, adict, NoneValue=""):
        self.dict = adict
        self.NoneValue = NoneValue

    def __getitem__(self, key):
        value = self.dict[key]
        if value is None:
            return self.NoneValue
        else:
            return value


def group_by_key(d, key):
    g = DictDefault(defaultvalue=[])
    for e in d:
        g[e[key]].append(e)
    return g


# Admissions des étudiants
# Différents types de voies d'admission:
# (stocké en texte libre dans la base, mais saisie par menus pour harmoniser)
TYPE_ADMISSION_DEFAULT = "Inconnue"
TYPES_ADMISSION = (TYPE_ADMISSION_DEFAULT, "APB", "APB-PC", "CEF", "Direct")

# ---- Simple python utilities


def simplesqlquote(s, maxlen=50):
    """simple SQL quoting to avoid most SQL injection attacks.
    Note: we use this function in the (rare) cases where we have to
    construct SQL code manually"""
    s = s[:maxlen]
    s.replace("'", r"\'")
    s.replace(";", r"\;")
    for bad in ("select", "drop", ";", "--", "insert", "delete", "xp_"):
        s = s.replace(bad, "")
    return s


def unescape_html(s):
    """un-escape html entities"""
    s = s.strip().replace("&amp;", "&")
    s = s.replace("&lt;", "<")
    s = s.replace("&gt;", ">")
    return s


# test if obj is iterable (but not a string)
isiterable = lambda obj: getattr(obj, "__iter__", False)


def unescape_html_dict(d):
    """un-escape all dict values, recursively"""
    try:
        indices = d.keys()
    except:
        indices = range(len(d))
    for k in indices:
        v = d[k]
        if type(v) == types.StringType:
            d[k] = unescape_html(v)
        elif isiterable(v):
            unescape_html_dict(v)


def quote_xml_attr(data):
    """Escape &, <, >, quotes and double quotes"""
    return xml.sax.saxutils.escape(str(data), {"'": "&apos;", '"': "&quot;"})


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')

    <?xml version="1.0" encoding="utf-8"?>
    <infos id="1">
      <ues note="10" />
      <ues />
    </infos>

    """
    if not tagname:
        raise ValueError("invalid empty tagname !")
    if not doc:
        doc = jaxml.XML_document(encoding=SCO_ENCODING)
    scalar_types = [types.StringType, types.UnicodeType, types.IntType, types.FloatType]
    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) == types.ListType])
            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)


def is_valid_code_nip(s):
    """True si s peut être un code NIP: au moins 6 chiffres décimaux"""
    if not s:
        return False
    return re.match(r"^[0-9]{6,32}$", s)


def strnone(s):
    "convert s to string, '' if s is false"
    if s:
        return str(s)
    else:
        return ""


def stripquotes(s):
    "strip s from spaces and quotes"
    s = s.strip()
    if s and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")):
        s = s[1:-1]
    return s


def suppress_accents(s):
    "s is an ordinary string, encoding given by SCO_ENCODING"
    return str(suppression_diacritics(unicode(s, SCO_ENCODING)))


def sanitize_string(s):
    """s is an ordinary string, encoding given by SCO_ENCODING"
    suppress accents and chars interpreted in XML
    Irreversible (not a quote)

    For ids and some filenames
    """
    return (
        suppress_accents(s.translate(None, "'`\"<>!&\\ "))
        .replace(" ", "_")
        .replace("\t", "_")
    )


def make_filename(name):
    """Try to convert name to a reasonnable filename"""
    return suppress_accents(name).replace(" ", "_")


VALID_CARS = (
    "-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.!"  # no / !
)
VALID_CARS_SET = set(VALID_CARS)
VALID_EXP = re.compile("^[" + VALID_CARS + "]+$")


def sanitize_filename(filename):
    """Keep only valid chars
    used for archives filenames
    """
    sane = "".join([c for c in filename if c in VALID_CARS_SET])
    if len(sane) < 2:
        sane = time.strftime("%Y-%m-%d-%H%M%S") + "-" + sane
    return sane


def is_valid_filename(filename):
    """True if filename is safe"""
    return VALID_EXP.match(filename)


def sendCSVFile(REQUEST, data, filename):
    """publication fichier.
    (on ne doit rien avoir émis avant, car ici sont générés les entetes)
    """
    filename = (
        unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_")
    )
    REQUEST.RESPONSE.setHeader("content-type", CSV_MIMETYPE)
    REQUEST.RESPONSE.setHeader(
        "content-disposition", 'attachment; filename="%s"' % filename
    )
    return data


def sendPDFFile(REQUEST, data, filename):
    filename = (
        unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_")
    )
    if REQUEST:
        REQUEST.RESPONSE.setHeader("content-type", PDF_MIMETYPE)
        REQUEST.RESPONSE.setHeader(
            "content-disposition", 'attachment; filename="%s"' % filename
        )
    return data


class ScoDocJSONEncoder(json.JSONEncoder):
    def default(self, o):  # pylint: disable=E0202
        import sco_formsemestre

        # ScoDoc 7.22 n'utilise plus mx:
        if str(type(o)) == "<type 'mx.DateTime.DateTime'>":
            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):
            return str(o)
        else:
            return json.JSONEncoder.default(self, o)


def sendJSON(REQUEST, data):
    js = json.dumps(data, encoding=SCO_ENCODING, indent=1, cls=ScoDocJSONEncoder)
    if REQUEST:
        REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE)
    return js


def sendXML(REQUEST, data, tagname=None, force_outer_xml_tag=True):
    if type(data) != types.ListType:
        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()
    if REQUEST:
        REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE)
    return repr(doc)


def sendResult(REQUEST, data, name=None, format=None, force_outer_xml_tag=True):
    if format is None:
        return data
    elif format == "xml":  # name is outer tagname
        return sendXML(
            REQUEST, data, tagname=name, force_outer_xml_tag=force_outer_xml_tag
        )
    elif format == "json":
        return sendJSON(REQUEST, data)
    else:
        raise ValueError("invalid format: %s" % format)


# Get SVN version
def get_svn_version(path):
    if os.path.exists("/usr/bin/svnversion"):
        try:
            return os.popen("svnversion " + path).read().strip()
        except:
            return "non disponible (erreur de lecture)"
    else:
        return "non disponible"


# Simple string manipulations
# on utf-8 encoded python strings
# (yes, we should only use unicode strings, but... we use only strings)
def strupper(s):
    return s.decode(SCO_ENCODING).upper().encode(SCO_ENCODING)


def strlower(s):
    return s.decode(SCO_ENCODING).lower().encode(SCO_ENCODING)


def strcapitalize(s):
    return s.decode(SCO_ENCODING).capitalize().encode(SCO_ENCODING)


def abbrev_prenom(prenom):
    "Donne l'abreviation d'un prenom"
    # un peu lent, mais espère traiter tous les cas
    # Jean -> J.
    # Charles -> Ch.
    # Jean-Christophe -> J.-C.
    # Marie Odile -> M. O.
    prenom = prenom.decode(SCO_ENCODING).replace(".", " ").strip()
    if not prenom:
        return ""
    d = prenom[:3].upper()
    if d == "CHA":
        abrv = "Ch."  # 'Charles' donne 'Ch.'
        i = 3
    else:
        abrv = prenom[0].upper() + "."
        i = 1
    n = len(prenom)
    while i < n:
        c = prenom[i]
        if c == " " or c == "-" and i < n - 1:
            sep = c
            i += 1
            # gobbe tous les separateurs
            while i < n and (prenom[i] == " " or prenom[i] == "-"):
                if prenom[i] == "-":
                    sep = "-"
                i += 1
            if i < n:
                abrv += sep + prenom[i].upper() + "."
        i += 1
    return abrv.encode(SCO_ENCODING)


#
def timedate_human_repr():
    "representation du temps courant pour utilisateur: a localiser"
    return time.strftime("%d/%m/%Y à %Hh%M")


def annee_scolaire_repr(year, month):
    """representation de l'annee scolaire : '2009 - 2010'
    à partir d'une date.
    """
    if month > 7:  # apres le 1er aout
        return "%s - %s" % (year, year + 1)
    else:
        return "%s - %s" % (year - 1, year)


def annee_scolaire_debut(year, month):
    """Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord..."""
    if int(month) > 7:
        return int(year)
    else:
        return int(year) - 1


def sem_decale_str(sem):
    """'D' si semestre decalé, ou ''"""
    # considère "décalé" les semestre impairs commençant entre janvier et juin
    # et les pairs entre juillet et decembre
    if sem["semestre_id"] <= 0:
        return ""
    if (sem["semestre_id"] % 2 and sem["mois_debut_ord"] <= 6) or (
        not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6
    ):
        return "D"
    else:
        return ""


# Graphes (optionnel pour ne pas accroitre les dependances de ScoDoc)
try:
    import pydot

    WITH_PYDOT = True
except:
    WITH_PYDOT = False

if WITH_PYDOT:
    # check API (incompatible change after pydot version 0.9.10: scodoc install may use old or new version)
    junk_graph = pydot.Dot("junk")
    junk_graph.add_node(pydot.Node("a"))
    n = junk_graph.get_node("a")
    if type(n) == type([]):  # "modern" pydot

        def pydot_get_node(g, name):
            r = g.get_node(name)
            if not r:
                return r
            else:
                return r[0]

    else:  # very old pydot

        def pydot_get_node(g, name):
            return g.get_node(name)


from sgmllib import SGMLParser


class html2txt_parser(SGMLParser):
    """html2txt()"""

    def reset(self):
        """reset() --> initialize the parser"""
        SGMLParser.reset(self)
        self.pieces = []

    def handle_data(self, text):
        """handle_data(text) --> appends the pieces to self.pieces
        handles all normal data not between brackets "<>"
        """
        self.pieces.append(text)

    def handle_entityref(self, ref):
        """called for each entity reference, e.g. for "&copy;", ref will be
        "copy"
        Reconstruct the original entity reference.
        """
        if ref == "amp":
            self.pieces.append("&")

    def output(self):
        """Return processed HTML as a single string"""
        return " ".join(self.pieces)


def scodoc_html2txt(html):
    parser = html2txt_parser()
    parser.reset()
    parser.feed(html)
    parser.close()
    return parser.output()


def is_valid_mail(email):
    """True if well-formed email address"""
    return re.match(r"^.+@.+\..{2,3}$", email)


ICONSIZES = {}  # name : (width, height) cache image sizes


def icontag(name, file_format="png", **attrs):
    """tag HTML pour un icone.
    (dans les versions anterieures on utilisait Zope)
    Les icones sont des fichiers PNG dans .../static/icons
    Si la taille (width et height) n'est pas spécifiée, lit l'image
    pour la mesurer (et cache le résultat).
    """
    if ("width" not in attrs) or ("height" not in attrs):
        if name not in ICONSIZES:
            img_file = SCO_SRCDIR + "/static/icons/%s.%s" % (name, file_format)
            im = PILImage.open(img_file)
            width, height = im.size[0], im.size[1]
            ICONSIZES[name] = (width, height)  # cache
        else:
            width, height = ICONSIZES[name]
        attrs["width"] = width
        attrs["height"] = height
    if "border" not in attrs:
        attrs["border"] = 0
    if "alt" not in attrs:
        attrs["alt"] = "logo %s" % name
    s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs])
    return '<img class="%s" %s src="/ScoDoc/static/icons/%s.%s" />' % (
        name,
        s,
        name,
        file_format,
    )


ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
ICON_XLS = icontag("xlsicon_img", title="Version tableur")


def sort_dates(L, reverse=False):
    """Return sorted list of dates, allowing None items (they are put at the beginning)"""
    mindate = datetime.datetime(datetime.MINYEAR, 1, 1)
    try:
        return sorted(L, key=lambda x: x or mindate, reverse=reverse)
    except:
        # Helps debugging
        log("sort_dates( %s )" % L)
        raise


def query_portal(req, msg="Portail Apogee", timeout=3):
    """Retreives external data using HTTP request
    (used to connect to Apogee portal, or ScoDoc server)
    returns a string,  "" on error
    """
    log("query_portal: %s" % req)

    try:
        f = urllib2.urlopen(req, timeout=timeout)  # seconds / request
    except:
        log("query_portal: can't connect to %s" % msg)
        return ""
    try:
        data = f.read()
    except:
        log("query_portal: error reading from %s" % msg)
        data = ""

    return data


def AnneeScolaire(REQUEST=None):
    "annee de debut de l'annee scolaire courante"
    if REQUEST and REQUEST.form.has_key("sco_year"):
        year = REQUEST.form["sco_year"]
        try:
            year = int(year)
            if year > 1900 and year < 2999:
                return year
        except:
            pass
    t = time.localtime()
    year, month = t[0], t[1]
    if month < 8:  # le "pivot" est le 1er aout
        year = year - 1
    return year


def log_unknown_etud(context, REQUEST=None, format="html"):
    """Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
    etudid = REQUEST.form.get("etudid", "?")
    code_nip = REQUEST.form.get("code_nip", "?")
    code_ine = REQUEST.form.get("code_ine", "?")
    log(
        "unknown student: etudid=%s code_nip=%s code_ine=%s"
        % (etudid, code_nip, code_ine)
    )
    return context.ScoErrorResponse("unknown student", format=format, REQUEST=REQUEST)


# XXX
# OK mais ne fonctione pas avec le publisher du vieux Zope car la signature de la méthode change
# def zope_method_str_or_json(func):
#     """Decorateur: pour les méthodes publiées qui ramène soit du texte (HTML) soit du JSON
#     sauf quand elles sont appellées depuis python.
#     La présence de l'argument REQUEST indique la publication.
#     """
#     def wrapper(*args, **kwargs):
#         r = func(*args, **kwargs)
#         REQUEST = kwargs.get('REQUEST', None)
#         # Published on the web but not string => convert to JSON
#         if REQUEST and not isinstance(r, six.string_types):
#             return sendJSON(REQUEST, r)
#         return r
#     wrapper.__doc__ = func.__doc__
#     return wrapper


def return_text_if_published(val, REQUEST):
    """Pour les méthodes publiées qui ramènent soit du texte (HTML) soit du JSON
    sauf quand elles sont appellées depuis python.
    La présence de l'argument REQUEST indique la publication.
    """
    if REQUEST and not isinstance(val, STRING_TYPES):
        return sendJSON(REQUEST, val)
    return val